From 3192aae20bc24ec02d5b2861c0ace4a2a254e73e Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 01:19:15 +0800 Subject: [PATCH 01/14] update: install script for cli --- install-cli.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 install-cli.sh diff --git a/install-cli.sh b/install-cli.sh new file mode 100644 index 0000000..4bfb09b --- /dev/null +++ b/install-cli.sh @@ -0,0 +1,81 @@ +# Installation script for Openconnect CLI +# This script will download the Openconnect CLI and vpnc-script and install them in $HOME/.oidcvpn/bin +# It will also add $HOME/.oidcvpn/bin to PATH +# Usage: +# curl -s -L URL_TO_SCRIPT_HERE | bash + +CLI_DOWNLOAD_URL="" +VPNC_SCRIPT_URL="https://gitlab.com/openconnect/vpnc-scripts/raw/master/vpnc-script" + +# detect os +if [[ "$OSTYPE" == "darwin"* ]]; then + # detect arch + if [[ "$HOSTTYPE" == "x86_64" ]]; then + # install macos cli + echo "installing macos cli for x86_64" + CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_osx-x86_64" + elif [[ "$HOSTTYPE" == "arm64" ]]; then + # install macos cli + echo "installing macos cli for arm64" + CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_osx-aarch64" + else + echo "unsupported arch" + exit 1 + fi + +elif [[ "$OSTYPE" == "linux-gnu" ]]; then + if [[ "$HOSTTYPE" == "x86_64" ]]; then + echo "installing linux cli" + CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_linux-x86_64" + else + echo "unsupported arch" + exit 1 + fi + +else + echo "unsupported os" + exit 1 +fi + +# check if .oidcvpn/bin folder exists under home directory +if [ ! -d "$HOME/.oidcvpn/bin" ]; then + mkdir -p $HOME/.oidcvpn/bin +fi + +# download cli +echo "Downloading cli" +curl -L $CLI_DOWNLOAD_URL >$HOME/.oidcvpn/bin/openconnect +chmod +x $HOME/.oidcvpn/bin/openconnect + +# download vpnc-script +echo "Downloading vpnc-script" +curl -L $VPNC_SCRIPT_URL >$HOME/.oidcvpn/bin/vpnc-script +chmod +x $HOME/.oidcvpn/bin/vpnc-script + +# add .oidcvpn/bin to PATH +echo "Checking if .oidcvpn/bin is in PATH" + +if [[ ":$PATH:" != *":$HOME/.oidcvpn/bin:"* ]]; then + + echo "Adding .oidcvpn/bin to PATH" + + # check if .bashrc or .bash_profile exists + if [ -f "$HOME/.bashrc" ]; then + echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" >>$HOME/.bashrc + echo "Run source $HOME/.bashrc to apply changes" + elif [ -f "$HOME/.bash_profile" ]; then + echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" >>$HOME/.bash_profile + echo "Run source $HOME/.bash_profile to apply changes" + fi + + # check if .zshrc exists + if [ -f "$HOME/.zshrc" ]; then + echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" >>$HOME/.zshrc + echo "Run source $HOME/.zshrc to apply changes" + fi + + echo "If you are using shell other than bash or zsh, please add the following line to your shell profile:" + echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" +fi + +echo "Installation complete" From c2fff8bd19eaf91c29ed195ad059fa78fdc11064 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 01:21:04 +0800 Subject: [PATCH 02/14] update: install script for cli --- install-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-cli.sh b/install-cli.sh index 4bfb09b..9589508 100644 --- a/install-cli.sh +++ b/install-cli.sh @@ -2,7 +2,7 @@ # This script will download the Openconnect CLI and vpnc-script and install them in $HOME/.oidcvpn/bin # It will also add $HOME/.oidcvpn/bin to PATH # Usage: -# curl -s -L URL_TO_SCRIPT_HERE | bash +# curl -s -L URL_TO_SCRIPT_HERE | sh CLI_DOWNLOAD_URL="" VPNC_SCRIPT_URL="https://gitlab.com/openconnect/vpnc-scripts/raw/master/vpnc-script" From 572571daad7cee0910badf189e9627ff88baebff Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 01:26:51 +0800 Subject: [PATCH 03/14] update: vpncscript path for cli --- Cargo.lock | 1 + crates/openconnect-cli/Cargo.toml | 1 + crates/openconnect-cli/src/main.rs | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 501cca1..58ea1eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2824,6 +2824,7 @@ dependencies = [ "clap", "comfy-table", "futures", + "home", "libc", "openconnect-core", "serde", diff --git a/crates/openconnect-cli/Cargo.toml b/crates/openconnect-cli/Cargo.toml index 4b2cd2c..99726b8 100644 --- a/crates/openconnect-cli/Cargo.toml +++ b/crates/openconnect-cli/Cargo.toml @@ -18,4 +18,5 @@ sudo = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } chrono = { workspace = true } +home = { workspace = true } comfy-table = "7.1.1" \ No newline at end of file diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index 0702549..82f0394 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -46,9 +46,12 @@ async fn connect_password_server( stored_configs: &StoredConfigs, ) -> Result, Box> { let password_server = password_server.decrypted_by(&stored_configs.cipher); + let homedir = home::home_dir().ok_or("Failed to get home directory")?; + let vpncscript = homedir.join(".oidcvpn/bin/vpnc-script"); + let vpncscript = vpncscript.to_str().ok_or("Failed to get vpncscript path")?; let config = ConfigBuilder::default() - .vpncscript("/opt/vpnc-scripts/vpnc-script") + .vpncscript(vpncscript) .loglevel(LogLevel::Info) .build()?; From 2956b943ab155c17c7188df05c7a201aed8032e0 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 11:36:14 +0800 Subject: [PATCH 04/14] build: strip error when building linux x64 --- crates/openconnect-sys/build/lib_prob.rs | 2 ++ crates/openconnect-sys/build/main.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/openconnect-sys/build/lib_prob.rs b/crates/openconnect-sys/build/lib_prob.rs index 7807cd0..1a6e3ec 100644 --- a/crates/openconnect-sys/build/lib_prob.rs +++ b/crates/openconnect-sys/build/lib_prob.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + #[macro_export] macro_rules! print_build_warning { ($($arg:tt)*) => { diff --git a/crates/openconnect-sys/build/main.rs b/crates/openconnect-sys/build/main.rs index 5751930..c255a42 100644 --- a/crates/openconnect-sys/build/main.rs +++ b/crates/openconnect-sys/build/main.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports)] + mod download_prebuilt; mod lib_prob; From 29174acc81732675d5ba11f83cfee9a7d02d37e8 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 11:45:35 +0800 Subject: [PATCH 05/14] chore: readme --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40c0666..d313cf7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ This is a cross-platform GUI client for OpenConnect, written in Rust and designe Edit Connect +## Installation + +- GUI: + + - Supports Windows(x64), Linux(x64), and macOS(aarch64, x64) + + - Download can be found in [Releases](https://github.com/hlhr202/Openconnect-RS/releases) + +- CLI install: + + - Only supports Linux(x64) and macOS(aarch64, x64) + + - Run the following command in your terminal: + + ```bash + curl -sL https://raw.githubusercontent.com/hlhr202/Openconnect-RS/main/install-cli.sh | sh + ``` + ## Build - Read the [System Requirements](./crates/openconnect-sys/README.md) for environment setup @@ -67,5 +85,9 @@ Special thanks to (MORE THAN) the following projects and technologies for making - [x] implement password login - [x] implement oidc login - [x] implement logs - - [ ] waiting tracing file rotation -- [ ] implement CLI + - [x] tracing file rotation +- [x] implement CLI + - [x] Add/Remove configurations + - [x] Daemon mode + - [x] Password login + - [ ] OIDC login From 93f499f2a8e47d7d844a03873285910582f4585e Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 12:11:19 +0800 Subject: [PATCH 06/14] chore: readme --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++- install-cli.sh | 59 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d313cf7..2620e09 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is a cross-platform GUI client for OpenConnect, written in Rust and designe Edit Connect -## Installation +## Installation of Client - GUI: @@ -37,6 +37,59 @@ This is a cross-platform GUI client for OpenConnect, written in Rust and designe curl -sL https://raw.githubusercontent.com/hlhr202/Openconnect-RS/main/install-cli.sh | sh ``` +## Usage of CLI client + +- Run the following command in your terminal: + + ```bash + openconnect --help + ``` + + This will print the following help message: + + ```plaintext + A CLI client to connect to VPN using OpenConnect + + Usage: openconnect + + Commands: + start Connect to a VPN server and run in daemon mode [aliases: connect, run] + status Get the current VPN connection status [aliases: info, stat] + stop Close the current connection and exit the daemon process [aliases: kill, disconnect] + add Add new VPN server configuration to local config file [aliases: new, create, insert] + delete Delete a VPN server configuration from local config file [aliases: rm, remove, del] + list List all VPN server configurations in local config file [aliases: ls, l] + logs Show logs of the daemon process [aliases: log] + help Print this message or the help of the given subcommand(s) + + Options: + -h, --help Print help + -V, --version Print version + ``` + +- For each subcommand, you can run `openconnect --help` to get more information + + For example: + + ```bash + openconnect start --help + ``` + + This will print the following help message: + + ```plaintext + Connect to a VPN server and run in daemon mode + + Usage: openconnect start [OPTIONS] + + Arguments: + The server name saved in local config file to connect to + + Options: + -c, --config-file The path to the local config file + -h, --help Print help + ``` + ## Build - Read the [System Requirements](./crates/openconnect-sys/README.md) for environment setup diff --git a/install-cli.sh b/install-cli.sh index 9589508..537e5cf 100644 --- a/install-cli.sh +++ b/install-cli.sh @@ -1,9 +1,35 @@ -# Installation script for Openconnect CLI -# This script will download the Openconnect CLI and vpnc-script and install them in $HOME/.oidcvpn/bin +#!/bin/sh +# Installation script for Openconnect-RS CLI +# This script will download the Openconnect-RS CLI and vpnc-script and install them in $HOME/.oidcvpn/bin # It will also add $HOME/.oidcvpn/bin to PATH # Usage: # curl -s -L URL_TO_SCRIPT_HERE | sh +COLOR_PRIMARY="\033[0;34m" +COLOR_WARN="\033[1;33m" +COLOR_SUCCESS="\033[0;32m" +COLOR_RESET="\033[0m" + +echo "" +echo "==================================" +echo "" +echo "${COLOR_PRIMARY}Installing Openconnect-RS CLI${COLOR_RESET}" +echo "" +echo "" +echo "This script will download the Openconnect CLI and vpnc-script and install them in $HOME/.oidcvpn/bin" +echo "${COLOR_WARN}WARNING: Openconnect-RS CLI has the same installed binary name as the original Openconnect CLI." +echo "Please remove the original Openconnect CLI if you wish to use Openconnect-RS CLI.${COLOR_RESET}" +echo "" +echo "==================================" +echo "" + +# shut down if openconnect is running +if pgrep -x "openconnect" > /dev/null +then + echo "Openconnect is running. Please shut it down before installing Openconnect-RS CLI" + exit 1 +fi + CLI_DOWNLOAD_URL="" VPNC_SCRIPT_URL="https://gitlab.com/openconnect/vpnc-scripts/raw/master/vpnc-script" @@ -12,11 +38,9 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # detect arch if [[ "$HOSTTYPE" == "x86_64" ]]; then # install macos cli - echo "installing macos cli for x86_64" CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_osx-x86_64" elif [[ "$HOSTTYPE" == "arm64" ]]; then # install macos cli - echo "installing macos cli for arm64" CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_osx-aarch64" else echo "unsupported arch" @@ -25,7 +49,6 @@ if [[ "$OSTYPE" == "darwin"* ]]; then elif [[ "$OSTYPE" == "linux-gnu" ]]; then if [[ "$HOSTTYPE" == "x86_64" ]]; then - echo "installing linux cli" CLI_DOWNLOAD_URL="https://github.com/hlhr202/Openconnect-RS/releases/download/v0.0.0-pre1/openconnect-cli_linux-x86_64" else echo "unsupported arch" @@ -43,18 +66,26 @@ if [ ! -d "$HOME/.oidcvpn/bin" ]; then fi # download cli -echo "Downloading cli" +echo "${COLOR_PRIMARY}Downloading cli${COLOR_RESET}" +echo "" curl -L $CLI_DOWNLOAD_URL >$HOME/.oidcvpn/bin/openconnect chmod +x $HOME/.oidcvpn/bin/openconnect +echo "" +echo "==================================" +echo "" # download vpnc-script -echo "Downloading vpnc-script" +echo "${COLOR_PRIMARY}Downloading vpnc-script${COLOR_RESET}" +echo "" curl -L $VPNC_SCRIPT_URL >$HOME/.oidcvpn/bin/vpnc-script chmod +x $HOME/.oidcvpn/bin/vpnc-script +echo "" +echo "==================================" +echo "" # add .oidcvpn/bin to PATH -echo "Checking if .oidcvpn/bin is in PATH" - +echo "${COLOR_PRIMARY}Adding .oidcvpn/bin to PATH${COLOR_RESET}" +echo "" if [[ ":$PATH:" != *":$HOME/.oidcvpn/bin:"* ]]; then echo "Adding .oidcvpn/bin to PATH" @@ -74,8 +105,12 @@ if [[ ":$PATH:" != *":$HOME/.oidcvpn/bin:"* ]]; then echo "Run source $HOME/.zshrc to apply changes" fi - echo "If you are using shell other than bash or zsh, please add the following line to your shell profile:" - echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" fi +echo "If you are using shell other than bash or zsh, please add the following line to your shell profile:" +echo "export PATH=\$PATH:$HOME/.oidcvpn/bin" + +echo "" +echo "==================================" +echo "" -echo "Installation complete" +echo "${COLOR_SUCCESS}Installation complete!${COLOR_RESET}" From fcb0c23f5c44da8459797deb20f37dafa34a737d Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Sun, 14 Apr 2024 20:32:39 +0800 Subject: [PATCH 07/14] feat: impl import config, refactor --- Cargo.lock | 40 ++++ Cargo.toml | 1 + crates/openconnect-cli/Cargo.toml | 4 +- crates/openconnect-cli/src/cli.rs | 10 +- crates/openconnect-cli/src/config.rs | 273 +++++++++++++++++++++++++++ crates/openconnect-cli/src/main.rs | 219 ++------------------- crates/openconnect-cli/src/sock.rs | 14 +- crates/openconnect-cli/src/state.rs | 42 +++++ 8 files changed, 392 insertions(+), 211 deletions(-) create mode 100644 crates/openconnect-cli/src/config.rs create mode 100644 crates/openconnect-cli/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 58ea1eb..742c094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -637,6 +637,19 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -977,6 +990,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -1146,6 +1172,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -2820,9 +2852,11 @@ dependencies = [ name = "openconnect-cli" version = "0.1.1" dependencies = [ + "base64 0.22.0", "chrono", "clap", "comfy-table", + "dialoguer", "futures", "home", "libc", @@ -4155,6 +4189,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index cc00e0f..3b8ba59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ reqwest = { version = "0.12.2", features = [ "blocking", "cookies", ] } +base64 = "0.22.0" clap = { version = "4.5.4", features = ["derive"] } tokio = { version = "1.37.0", features = ["full"] } tokio-serde = { version = "0.9.0", features = ["json", "serde"] } diff --git a/crates/openconnect-cli/Cargo.toml b/crates/openconnect-cli/Cargo.toml index 99726b8..5e6e160 100644 --- a/crates/openconnect-cli/Cargo.toml +++ b/crates/openconnect-cli/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.1" edition = "2021" [dependencies] +base64 = { workspace = true } clap = { workspace = true } tokio = { workspace = true } tokio-serde = { workspace = true } @@ -19,4 +20,5 @@ tracing = { workspace = true } thiserror = { workspace = true } chrono = { workspace = true } home = { workspace = true } -comfy-table = "7.1.1" \ No newline at end of file +comfy-table = "7.1.1" +dialoguer = "0.11.0" \ No newline at end of file diff --git a/crates/openconnect-cli/src/cli.rs b/crates/openconnect-cli/src/cli.rs index 3fe7b77..9e77008 100644 --- a/crates/openconnect-cli/src/cli.rs +++ b/crates/openconnect-cli/src/cli.rs @@ -34,7 +34,13 @@ pub enum Commands { about = "Add new VPN server configuration to local config file", visible_aliases = ["new", "create", "insert"] )] - Add(ServerType), + Add(SeverConfigArgs), + + #[command(about = "Import VPN server configurations from a base64 encoded string")] + Import { + /// The base64 encoded string of the VPN server configurations + base64: String, + }, #[command(about = "Delete a VPN server configuration from local config file", visible_aliases = ["rm", "remove", "del"])] Delete { @@ -50,7 +56,7 @@ pub enum Commands { } #[derive(Subcommand, Debug)] -pub enum ServerType { +pub enum SeverConfigArgs { #[command(about = "Add an OIDC authentication VPN server")] Oidc { #[arg(short, long)] diff --git a/crates/openconnect-cli/src/config.rs b/crates/openconnect-cli/src/config.rs new file mode 100644 index 0000000..df9dc0c --- /dev/null +++ b/crates/openconnect-cli/src/config.rs @@ -0,0 +1,273 @@ +use crate::cli::SeverConfigArgs; +use base64::Engine; +use comfy_table::Table; +use openconnect_core::storage::{OidcServer, PasswordServer, StoredConfigs, StoredServer}; +use std::{error::Error, path::PathBuf}; + +pub async fn get_server_config( + server_name: &str, + config_file: PathBuf, +) -> Result<(StoredServer, StoredConfigs), Box> { + let mut stored_configs = StoredConfigs::new(None, config_file); + let config = stored_configs.read_from_file().await?; + let server = config.servers.get(server_name); + + match server { + Some(server) => { + match server { + StoredServer::Oidc(OidcServer { server, .. }) => { + println!("Connecting to OIDC server: {}", server_name); + println!("Server host: {}", server); + } + StoredServer::Password(PasswordServer { server, .. }) => { + println!("Connecting to password server: {}", server_name); + println!("Server host: {}", server); + } + } + Ok((server.clone(), stored_configs)) + } + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Server {} not found", server_name), + ))?, + } +} + +fn add_server_internal(stored_server: StoredServer) { + let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + runtime.block_on(async { + let mut stored_configs = StoredConfigs::new(None, config_file); + + stored_configs + .read_from_file() + .await + .expect("Failed to read config file"); + + stored_configs + .upsert_server(stored_server) + .await + .expect("Failed to add server"); + }); +} + +pub fn add_server(server_config: SeverConfigArgs) { + let new_server = match server_config { + SeverConfigArgs::Oidc { + name, + server, + issuer, + client_id, + client_secret, + allow_insecure, + } => { + let oidc_server = OidcServer { + name, + server, + issuer, + client_id, + client_secret, + allow_insecure, + updated_at: None, + }; + + StoredServer::Oidc(oidc_server) + } + SeverConfigArgs::Password { + name, + server, + username, + password, + allow_insecure, + } => { + let password_server = PasswordServer { + name, + server, + username, + password: Some(password), + allow_insecure, + updated_at: None, + }; + + StoredServer::Password(password_server) + } + }; + + add_server_internal(new_server); +} + +pub fn delete_server(name: &str) { + let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + runtime.block_on(async { + let mut stored_configs = StoredConfigs::new(None, config_file); + + stored_configs + .read_from_file() + .await + .expect("Failed to read config file"); + + stored_configs + .remove_server(name) + .await + .expect("Failed to delete server"); + }); +} + +pub fn list_servers() { + let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + runtime.block_on(async { + let mut stored_configs = StoredConfigs::new(None, config_file); + + let stored_configs = stored_configs.read_from_file().await.unwrap(); + let mut table = Table::new(); + table.set_header(vec![ + "Name".to_string(), + "Type".to_string(), + "Server".to_string(), + "Allow Insecure".to_string(), + "Updated At".to_string(), + ]); + + for (name, server) in stored_configs.servers.iter() { + match server { + StoredServer::Oidc(OidcServer { + server, + allow_insecure, + updated_at, + .. + }) => { + table.add_row(vec![ + name.clone(), + "OIDC Server".to_string(), + server.clone(), + allow_insecure.unwrap_or(false).to_string(), + updated_at.as_ref().unwrap_or(&"".to_string()).to_owned(), + ]); + } + StoredServer::Password(PasswordServer { + server, + allow_insecure, + updated_at, + .. + }) => { + table.add_row(vec![ + name.clone(), + "Password Server".to_string(), + server.clone(), + allow_insecure.unwrap_or(false).to_string(), + updated_at.as_ref().unwrap_or(&"".to_string()).to_owned(), + ]); + } + } + } + + println!("{table}"); + }); +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase", tag = "authType")] +pub enum PartialImportServer { + #[serde(rename_all = "camelCase")] + Oidc { + server: String, + allow_insecure: Option, + issuer: String, + client_id: String, + client_secret: Option, + }, + #[serde(rename_all = "camelCase")] + Password { + server: String, + allow_insecure: Option, + }, +} + +pub fn import_server(base64: &str) { + let decoded = base64::prelude::BASE64_STANDARD + .decode(base64.as_bytes()) + .expect("Failed to decode base64"); + + let string = String::from_utf8(decoded).expect("Failed to convert to string"); + + let server: PartialImportServer = + serde_json::from_str(&string).expect("Failed to parse your import string"); + + println!("=============================================="); + println!("Existing configs: {:#?}\n", server); + + let new_server = match server { + PartialImportServer::Password { + server, + allow_insecure, + } => { + println!("We still need some extra information to complete the import"); + println!("==============================================\n"); + // prompt for servername, username and password + + println!("Enter an unique server name, this will be used as an identifier for the local config file"); + let name = dialoguer::Input::::new() + .with_prompt("Server name") + .interact() + .expect("Failed to get server name"); + + let username = dialoguer::Input::::new() + .with_prompt("Enter username") + .interact() + .expect("Failed to get username"); + + let password = dialoguer::Password::new() + .with_prompt("Enter password") + .interact() + .expect("Failed to get password"); + + StoredServer::Password(PasswordServer { + name, + server, + username, + password: Some(password), + allow_insecure, + updated_at: None, + }) + } + PartialImportServer::Oidc { + server, + allow_insecure, + issuer, + client_id, + client_secret, + } => { + println!("We still need some information to complete the import"); + println!("==============================================\n"); + + println!("Enter an unique server name, this will be used as an identifier for the local config file"); + let name = dialoguer::Input::::new() + .with_prompt("Server name") + .interact() + .expect("Failed to get server name"); + + StoredServer::Oidc(OidcServer { + name, + server, + issuer, + client_id, + client_secret, + allow_insecure, + updated_at: None, + }) + } + }; + + add_server_internal(new_server); +} + +#[test] +fn test_import_server() { + // "{"authType":"oidc","server":"https://example.com","issuer":"https://example.com","clientId":"12345","clientSecret":"123456","allowInsecure":true}" + import_server("eyJhdXRoVHlwZSI6Im9pZGMiLCJzZXJ2ZXIiOiJodHRwczovL2V4YW1wbGUuY29tIiwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImNsaWVudElkIjoiMTIzNDUiLCJjbGllbnRTZWNyZXQiOiIxMjM0NTYiLCJhbGxvd0luc2VjdXJlIjp0cnVlfQ=="); +} diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index 82f0394..bacc556 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -1,18 +1,19 @@ mod cli; +mod config; mod daemon; mod sock; +mod state; -use crate::sock::Server; +use crate::sock::UnixDomainServer; +use crate::state::connect_password_server; use clap::Parser; -use cli::{Cli, Commands, ServerType}; +use cli::{Cli, Commands}; use comfy_table::Table; use futures::{SinkExt, TryStreamExt}; use openconnect_core::{ - config::{ConfigBuilder, EntrypointBuilder, LogLevel}, - events::EventHandlers, ip_info::IpInfo, log::Logger, - storage::{OidcServer, PasswordServer, StoredConfigs, StoredServer}, + storage::{StoredConfigs, StoredServer}, Connectable, Status, VpnClient, }; use std::{error::Error, io::BufRead, path::PathBuf, sync::Arc}; @@ -41,41 +42,6 @@ pub enum JsonResponse { }, } -async fn connect_password_server( - password_server: &PasswordServer, - stored_configs: &StoredConfigs, -) -> Result, Box> { - let password_server = password_server.decrypted_by(&stored_configs.cipher); - let homedir = home::home_dir().ok_or("Failed to get home directory")?; - let vpncscript = homedir.join(".oidcvpn/bin/vpnc-script"); - let vpncscript = vpncscript.to_str().ok_or("Failed to get vpncscript path")?; - - let config = ConfigBuilder::default() - .vpncscript(vpncscript) - .loglevel(LogLevel::Info) - .build()?; - - let entrypoint = EntrypointBuilder::new() - .name(&password_server.name) - .server(&password_server.server) - .username(&password_server.username) - .password(&password_server.password.clone().unwrap_or("".to_string())) - .accept_insecure_cert(password_server.allow_insecure.unwrap_or(false)) - .enable_udp(true) - .build()?; - - let event_handler = EventHandlers::default(); - - let client = VpnClient::new(config, event_handler)?; - let client_clone = client.clone(); - - tokio::task::spawn_blocking(move || { - let _ = client_clone.connect(entrypoint); - }); - - Ok(client) -} - async fn try_accept(listener: &tokio::net::UnixListener, client: Arc) { if let Ok((stream, _)) = listener.accept().await { let (read, write) = stream.into_split(); @@ -135,7 +101,7 @@ async fn start_daemon( stored_server: &StoredServer, stored_configs: &StoredConfigs, ) -> Result<(), Box> { - let server = Server::bind()?; + let server = UnixDomainServer::bind()?; let mut sigterm = signal(SignalKind::terminate())?; let mut sigint = signal(SignalKind::interrupt())?; let mut sigquit = signal(SignalKind::quit())?; @@ -169,180 +135,31 @@ async fn start_daemon( Ok(()) } -async fn get_server( - server_name: &str, - config_file: PathBuf, -) -> Result<(StoredServer, StoredConfigs), Box> { - let mut stored_configs = StoredConfigs::new(None, config_file); - let config = stored_configs.read_from_file().await?; - let server = config.servers.get(server_name); - - match server { - Some(server) => { - match server { - StoredServer::Oidc(OidcServer { server, .. }) => { - println!("Connecting to OIDC server: {}", server_name); - println!("Server host: {}", server); - } - StoredServer::Password(PasswordServer { server, .. }) => { - println!("Connecting to password server: {}", server_name); - println!("Server host: {}", server); - } - } - Ok((server.clone(), stored_configs)) - } - None => Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Server {} not found", server_name), - ))?, - } -} - fn main() { let cli = Cli::parse(); match cli.command { - Commands::Add(server_type) => { - let new_server = match server_type { - ServerType::Oidc { - name, - server, - issuer, - client_id, - client_secret, - allow_insecure, - } => { - let oidc_server = OidcServer { - name, - server, - issuer, - client_id, - client_secret, - allow_insecure, - updated_at: None, - }; - - StoredServer::Oidc(oidc_server) - } - ServerType::Password { - name, - server, - username, - password, - allow_insecure, - } => { - let password_server = PasswordServer { - name, - server, - username, - password: Some(password), - allow_insecure, - updated_at: None, - }; - - StoredServer::Password(password_server) - } - }; - - let config_file = - StoredConfigs::getorinit_config_file().expect("Failed to get config file"); - - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - runtime.block_on(async { - let mut stored_configs = StoredConfigs::new(None, config_file); - - stored_configs - .read_from_file() - .await - .expect("Failed to read config file"); + Commands::Add(server_config) => { + crate::config::add_server(server_config); + } - stored_configs - .upsert_server(new_server) - .await - .expect("Failed to add server"); - }); + Commands::Import { base64 } => { + crate::config::import_server(&base64); } Commands::Delete { name } => { - let config_file = - StoredConfigs::getorinit_config_file().expect("Failed to get config file"); - - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - runtime.block_on(async { - let mut stored_configs = StoredConfigs::new(None, config_file); - - stored_configs - .read_from_file() - .await - .expect("Failed to read config file"); - - stored_configs - .remove_server(&name) - .await - .expect("Failed to delete server"); - }); + crate::config::delete_server(&name); } Commands::List => { - let config_file = - StoredConfigs::getorinit_config_file().expect("Failed to get config file"); - - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - runtime.block_on(async { - let mut stored_configs = StoredConfigs::new(None, config_file); - - let stored_configs = stored_configs.read_from_file().await.unwrap(); - let mut table = Table::new(); - table.set_header(vec![ - "Name".to_string(), - "Type".to_string(), - "Server".to_string(), - "Allow Insecure".to_string(), - "Updated At".to_string(), - ]); - - for (name, server) in stored_configs.servers.iter() { - match server { - StoredServer::Oidc(OidcServer { - server, - allow_insecure, - updated_at, - .. - }) => { - table.add_row(vec![ - name.clone(), - "OIDC Server".to_string(), - server.clone(), - allow_insecure.unwrap_or(false).to_string(), - updated_at.as_ref().unwrap_or(&"".to_string()).to_owned(), - ]); - } - StoredServer::Password(PasswordServer { - server, - allow_insecure, - updated_at, - .. - }) => { - table.add_row(vec![ - name.clone(), - "Password Server".to_string(), - server.clone(), - allow_insecure.unwrap_or(false).to_string(), - updated_at.as_ref().unwrap_or(&"".to_string()).to_owned(), - ]); - } - } - } - - println!("{table}"); - }); + crate::config::list_servers(); } Commands::Status => { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); runtime.block_on(async { - let client = sock::Client::connect().await; + let client = sock::UnixDomainClient::connect().await; match client { Ok(mut client) => { @@ -442,7 +259,7 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); runtime.block_on(async { - let client = sock::Client::connect().await; + let client = sock::UnixDomainClient::connect().await; match client { Ok(mut client) => { @@ -489,7 +306,7 @@ fn main() { daemon::ForkResult::Parent => { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); runtime.block_on(async { - match get_server(&name, config_file).await { + match crate::config::get_server_config(&name, config_file).await { Ok(_) => {} Err(e) => { println!("Failed to get server: {}", e); @@ -511,7 +328,7 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); let (server, configs) = runtime.block_on(async { - get_server(&name, config_file) + crate::config::get_server_config(&name, config_file) .await .expect("Failed to get server") }); diff --git a/crates/openconnect-cli/src/sock.rs b/crates/openconnect-cli/src/sock.rs index 9cb671f..2bfca35 100644 --- a/crates/openconnect-cli/src/sock.rs +++ b/crates/openconnect-cli/src/sock.rs @@ -44,33 +44,33 @@ pub fn exists() -> bool { get_sock().exists() } -pub struct Server { +pub struct UnixDomainServer { pub listener: UnixListener, } -impl Server { +impl UnixDomainServer { pub fn bind() -> Result { let listener = UnixListener::bind(get_sock())?; let listener = listener.into_std()?; listener.set_nonblocking(true)?; let listener = UnixListener::from_std(listener)?; - Ok(Server { listener }) + Ok(UnixDomainServer { listener }) } } -impl Drop for Server { +impl Drop for UnixDomainServer { fn drop(&mut self) { // There's no way to return a useful error here std::fs::remove_file(get_sock()).expect("Failed to remove socket file"); } } -pub struct Client { +pub struct UnixDomainClient { framed_writer: FramedWriter, pub framed_reader: FramedReader, } -impl Client { +impl UnixDomainClient { pub async fn connect() -> Result { let sock = get_sock(); if !sock.exists() { @@ -81,7 +81,7 @@ impl Client { let framed_writer = get_framed_writer(write); let framed_reader = get_framed_reader(read); - Ok(Client { + Ok(UnixDomainClient { framed_writer, framed_reader, }) diff --git a/crates/openconnect-cli/src/state.rs b/crates/openconnect-cli/src/state.rs new file mode 100644 index 0000000..304c1ff --- /dev/null +++ b/crates/openconnect-cli/src/state.rs @@ -0,0 +1,42 @@ +use openconnect_core::{ + config::{ConfigBuilder, EntrypointBuilder, LogLevel}, + events::EventHandlers, + storage::{PasswordServer, StoredConfigs}, + Connectable, VpnClient, +}; +use std::{error::Error, sync::Arc}; + +pub async fn connect_password_server( + password_server: &PasswordServer, + stored_configs: &StoredConfigs, +) -> Result, Box> { + let password_server = password_server.decrypted_by(&stored_configs.cipher); + let homedir = home::home_dir().ok_or("Failed to get home directory")?; + let vpncscript = homedir.join(".oidcvpn/bin/vpnc-script"); + let vpncscript = vpncscript.to_str().ok_or("Failed to get vpncscript path")?; + + let config = ConfigBuilder::default() + .vpncscript(vpncscript) + .loglevel(LogLevel::Info) + .build()?; + + let entrypoint = EntrypointBuilder::new() + .name(&password_server.name) + .server(&password_server.server) + .username(&password_server.username) + .password(&password_server.password.clone().unwrap_or("".to_string())) + .accept_insecure_cert(password_server.allow_insecure.unwrap_or(false)) + .enable_udp(true) + .build()?; + + let event_handler = EventHandlers::default(); + + let client = VpnClient::new(config, event_handler)?; + let client_clone = client.clone(); + + tokio::task::spawn_blocking(move || { + let _ = client_clone.connect(entrypoint); + }); + + Ok(client) +} From 841c7ae40b927255de880b20ea5f4b473dfbfd6e Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Mon, 15 Apr 2024 11:58:43 +0800 Subject: [PATCH 08/14] feat: impl export config, refactor to C/S arch --- crates/openconnect-cli/src/cli.rs | 6 ++ crates/openconnect-cli/src/config.rs | 82 +++++++++++++--- crates/openconnect-cli/src/main.rs | 140 ++++----------------------- crates/openconnect-cli/src/state.rs | 119 +++++++++++++++++++++++ 4 files changed, 214 insertions(+), 133 deletions(-) diff --git a/crates/openconnect-cli/src/cli.rs b/crates/openconnect-cli/src/cli.rs index 9e77008..eed3fee 100644 --- a/crates/openconnect-cli/src/cli.rs +++ b/crates/openconnect-cli/src/cli.rs @@ -42,6 +42,12 @@ pub enum Commands { base64: String, }, + #[command(about = "Export VPN server configurations to a base64 encoded string")] + Export { + /// The name of the VPN server configuration to export + name: String, + }, + #[command(about = "Delete a VPN server configuration from local config file", visible_aliases = ["rm", "remove", "del"])] Delete { /// The server name saved in local config file to delete diff --git a/crates/openconnect-cli/src/config.rs b/crates/openconnect-cli/src/config.rs index df9dc0c..3a38852 100644 --- a/crates/openconnect-cli/src/config.rs +++ b/crates/openconnect-cli/src/config.rs @@ -4,7 +4,7 @@ use comfy_table::Table; use openconnect_core::storage::{OidcServer, PasswordServer, StoredConfigs, StoredServer}; use std::{error::Error, path::PathBuf}; -pub async fn get_server_config( +pub async fn read_server_config_from_fs( server_name: &str, config_file: PathBuf, ) -> Result<(StoredServer, StoredConfigs), Box> { @@ -52,7 +52,7 @@ fn add_server_internal(stored_server: StoredServer) { }); } -pub fn add_server(server_config: SeverConfigArgs) { +pub fn request_add_server(server_config: SeverConfigArgs) { let new_server = match server_config { SeverConfigArgs::Oidc { name, @@ -97,7 +97,7 @@ pub fn add_server(server_config: SeverConfigArgs) { add_server_internal(new_server); } -pub fn delete_server(name: &str) { +pub fn request_delete_server(name: &str) { let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); @@ -116,7 +116,7 @@ pub fn delete_server(name: &str) { }); } -pub fn list_servers() { +pub fn request_list_servers() { let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); @@ -172,7 +172,7 @@ pub fn list_servers() { #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase", tag = "authType")] -pub enum PartialImportServer { +pub enum SharableServer { #[serde(rename_all = "camelCase")] Oidc { server: String, @@ -188,21 +188,72 @@ pub enum PartialImportServer { }, } -pub fn import_server(base64: &str) { +pub fn request_export_server(server_name: &str) { + let config_file = StoredConfigs::getorinit_config_file().expect("Failed to get config file"); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + runtime.block_on(async { + let mut stored_configs = StoredConfigs::new(None, config_file); + + stored_configs + .read_from_file() + .await + .expect("Failed to read config file"); + + let server = stored_configs.servers.get(server_name); + + match server { + Some(stored_server) => { + let base64 = match stored_server { + StoredServer::Oidc(oidc_server) => { + let oidc_server = oidc_server.clone(); + let partial_server = SharableServer::Oidc { + server: oidc_server.server, + allow_insecure: oidc_server.allow_insecure, + issuer: oidc_server.issuer, + client_id: oidc_server.client_id, + client_secret: oidc_server.client_secret, + }; + let json = + serde_json::to_string(&partial_server).expect("Failed to serialize"); + base64::prelude::BASE64_STANDARD.encode(json.as_bytes()) + } + StoredServer::Password(password_server) => { + let password_server = password_server.clone(); + let partial_server = SharableServer::Password { + server: password_server.server, + allow_insecure: password_server.allow_insecure, + }; + let json = + serde_json::to_string(&partial_server).expect("Failed to serialize"); + base64::prelude::BASE64_STANDARD.encode(json.as_bytes()) + } + }; + + println!("Share this: {}", base64); + } + None => { + eprintln!("Server {} not found", server_name); + } + } + }); +} + +pub fn request_import_server(base64: &str) { let decoded = base64::prelude::BASE64_STANDARD .decode(base64.as_bytes()) .expect("Failed to decode base64"); let string = String::from_utf8(decoded).expect("Failed to convert to string"); - let server: PartialImportServer = + let server: SharableServer = serde_json::from_str(&string).expect("Failed to parse your import string"); println!("=============================================="); println!("Existing configs: {:#?}\n", server); let new_server = match server { - PartialImportServer::Password { + SharableServer::Password { server, allow_insecure, } => { @@ -235,7 +286,7 @@ pub fn import_server(base64: &str) { updated_at: None, }) } - PartialImportServer::Oidc { + SharableServer::Oidc { server, allow_insecure, issuer, @@ -268,6 +319,15 @@ pub fn import_server(base64: &str) { #[test] fn test_import_server() { - // "{"authType":"oidc","server":"https://example.com","issuer":"https://example.com","clientId":"12345","clientSecret":"123456","allowInsecure":true}" - import_server("eyJhdXRoVHlwZSI6Im9pZGMiLCJzZXJ2ZXIiOiJodHRwczovL2V4YW1wbGUuY29tIiwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImNsaWVudElkIjoiMTIzNDUiLCJjbGllbnRTZWNyZXQiOiIxMjM0NTYiLCJhbGxvd0luc2VjdXJlIjp0cnVlfQ=="); + let partial_import_server = SharableServer::Oidc { + server: "https://example.com".to_string(), + allow_insecure: Some(true), + issuer: "https://example.com".to_string(), + client_id: "12345".to_string(), + client_secret: Some("123456".to_string()), + }; + + let json = serde_json::to_string(&partial_import_server).expect("Failed to serialize"); + let base64 = base64::prelude::BASE64_STANDARD.encode(json.as_bytes()); + request_import_server(&base64); } diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index bacc556..456ccf2 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -5,10 +5,8 @@ mod sock; mod state; use crate::sock::UnixDomainServer; -use crate::state::connect_password_server; use clap::Parser; use cli::{Cli, Commands}; -use comfy_table::Table; use futures::{SinkExt, TryStreamExt}; use openconnect_core::{ ip_info::IpInfo, @@ -108,7 +106,7 @@ async fn start_daemon( let client = match stored_server { StoredServer::Password(password_server) => { - connect_password_server(password_server, stored_configs).await? + crate::state::connect_password_server(password_server, stored_configs).await? } StoredServer::Oidc(_) => { panic!("OIDC server not implemented"); @@ -127,7 +125,7 @@ async fn start_daemon( break; } _ = try_accept(&server.listener, client.clone()) => { - + // noop } }; } @@ -140,102 +138,27 @@ fn main() { match cli.command { Commands::Add(server_config) => { - crate::config::add_server(server_config); + crate::config::request_add_server(server_config); } Commands::Import { base64 } => { - crate::config::import_server(&base64); + crate::config::request_import_server(&base64); + } + + Commands::Export { name } => { + crate::config::request_export_server(&name); } Commands::Delete { name } => { - crate::config::delete_server(&name); + crate::config::request_delete_server(&name); } Commands::List => { - crate::config::list_servers(); + crate::config::request_list_servers(); } Commands::Status => { - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - - runtime.block_on(async { - let client = sock::UnixDomainClient::connect().await; - - match client { - Ok(mut client) => { - client - .send(JsonRequest::Info) - .await - .expect("Failed to send info command"); - - if let Ok(Some(response)) = client.framed_reader.try_next().await { - match response { - JsonResponse::InfoResult { - server_name, - server_url, - hostname, - status, - info, - } => { - let mut table = Table::new(); - let mut rows = vec![ - vec![format!("Server Name"), server_name], - vec![format!("Server URL"), server_url], - vec![format!("Server IP"), hostname], - vec![format!("Connection Status"), status], - ]; - - if let Some(info) = info { - let addr = info.addr.unwrap_or("".to_string()); - let netmask = info.netmask.unwrap_or("".to_string()); - let addr6 = info.addr6.unwrap_or("".to_string()); - let netmask6 = info.netmask6.unwrap_or("".to_string()); - let dns1 = info.dns[0].clone().unwrap_or("".to_string()); - let dns2 = info.dns[1].clone().unwrap_or("".to_string()); - let dns3 = info.dns[2].clone().unwrap_or("".to_string()); - let nbns1 = info.nbns[0].clone().unwrap_or("".to_string()); - let nbns2 = info.nbns[1].clone().unwrap_or("".to_string()); - let nbns3 = info.nbns[2].clone().unwrap_or("".to_string()); - let domain = info.domain.unwrap_or("".to_string()); - let proxy_pac = info.proxy_pac.unwrap_or("".to_string()); - let mtu = info.mtu.to_string(); - let gateway_addr = - info.gateway_addr.clone().unwrap_or("".to_string()); - let info_rows = vec![ - vec![format!("IPv4 Address"), addr], - vec![format!("IPv4 Netmask"), netmask], - vec![format!("IPv6 Address"), addr6], - vec![format!("IPv6 Netmask"), netmask6], - vec![format!("DNS 1"), dns1], - vec![format!("DNS 2"), dns2], - vec![format!("DNS 3"), dns3], - vec![format!("NBNS 1"), nbns1], - vec![format!("NBNS 2"), nbns2], - vec![format!("NBNS 3"), nbns3], - vec![format!("Domain"), domain], - vec![format!("Proxy PAC"), proxy_pac], - vec![format!("MTU"), mtu], - vec![format!("Gateway Address"), gateway_addr], - ]; - - rows.extend(info_rows); - } - - table.add_rows(rows); - - println!("{table}"); - } - _ => { - println!("Received unexpected response"); - } - } - } - } - Err(e) => { - println!("Failed to connect to server: {}", e); - } - } - }); + crate::state::request_get_status(); } Commands::Logs => { @@ -256,41 +179,14 @@ fn main() { } Commands::Stop => { - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - - runtime.block_on(async { - let client = sock::UnixDomainClient::connect().await; - - match client { - Ok(mut client) => { - client - .send(JsonRequest::Stop) - .await - .expect("Failed to send stop command"); - - if let Ok(Some(response)) = client.framed_reader.try_next().await { - match response { - JsonResponse::StopResult { server_name } => { - println!("Stopped connection to server: {}", server_name) - } - _ => { - println!("Received unexpected response"); - } - } - } - } - Err(e) => { - println!("Failed to connect to server: {}", e); - } - }; - }); + crate::state::request_stop_server(); } Commands::Start { name, config_file } => { if sock::exists() { - println!("Socket already exists. You may have a connected VPN session or a stale socket file. You may solve by:"); - println!("1. Stopping the connection by sending stop command."); - println!( + eprintln!("Socket already exists. You may have a connected VPN session or a stale socket file. You may solve by:"); + eprintln!("1. Stopping the connection by sending stop command."); + eprintln!( "2. Manually deleting the socket file which located at: {}", sock::get_sock().display() ); @@ -306,10 +202,10 @@ fn main() { daemon::ForkResult::Parent => { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); runtime.block_on(async { - match crate::config::get_server_config(&name, config_file).await { + match crate::config::read_server_config_from_fs(&name, config_file).await { Ok(_) => {} Err(e) => { - println!("Failed to get server: {}", e); + eprintln!("Failed to get server: {}", e); std::process::exit(1); } } @@ -328,7 +224,7 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); let (server, configs) = runtime.block_on(async { - crate::config::get_server_config(&name, config_file) + crate::config::read_server_config_from_fs(&name, config_file) .await .expect("Failed to get server") }); diff --git a/crates/openconnect-cli/src/state.rs b/crates/openconnect-cli/src/state.rs index 304c1ff..5385afe 100644 --- a/crates/openconnect-cli/src/state.rs +++ b/crates/openconnect-cli/src/state.rs @@ -1,3 +1,6 @@ +use crate::{sock, JsonRequest, JsonResponse}; +use comfy_table::Table; +use futures::TryStreamExt; use openconnect_core::{ config::{ConfigBuilder, EntrypointBuilder, LogLevel}, events::EventHandlers, @@ -40,3 +43,119 @@ pub async fn connect_password_server( Ok(client) } + +pub fn request_get_status() { + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + + runtime.block_on(async { + let client = sock::UnixDomainClient::connect().await; + + match client { + Ok(mut client) => { + client + .send(JsonRequest::Info) + .await + .expect("Failed to send info command"); + + if let Ok(Some(response)) = client.framed_reader.try_next().await { + match response { + JsonResponse::InfoResult { + server_name, + server_url, + hostname, + status, + info, + } => { + let mut table = Table::new(); + let mut rows = vec![ + vec![format!("Server Name"), server_name], + vec![format!("Server URL"), server_url], + vec![format!("Server IP"), hostname], + vec![format!("Connection Status"), status], + ]; + + if let Some(info) = info { + let addr = info.addr.unwrap_or("".to_string()); + let netmask = info.netmask.unwrap_or("".to_string()); + let addr6 = info.addr6.unwrap_or("".to_string()); + let netmask6 = info.netmask6.unwrap_or("".to_string()); + let dns1 = info.dns[0].clone().unwrap_or("".to_string()); + let dns2 = info.dns[1].clone().unwrap_or("".to_string()); + let dns3 = info.dns[2].clone().unwrap_or("".to_string()); + let nbns1 = info.nbns[0].clone().unwrap_or("".to_string()); + let nbns2 = info.nbns[1].clone().unwrap_or("".to_string()); + let nbns3 = info.nbns[2].clone().unwrap_or("".to_string()); + let domain = info.domain.unwrap_or("".to_string()); + let proxy_pac = info.proxy_pac.unwrap_or("".to_string()); + let mtu = info.mtu.to_string(); + let gateway_addr = + info.gateway_addr.clone().unwrap_or("".to_string()); + let info_rows = vec![ + vec![format!("IPv4 Address"), addr], + vec![format!("IPv4 Netmask"), netmask], + vec![format!("IPv6 Address"), addr6], + vec![format!("IPv6 Netmask"), netmask6], + vec![format!("DNS 1"), dns1], + vec![format!("DNS 2"), dns2], + vec![format!("DNS 3"), dns3], + vec![format!("NBNS 1"), nbns1], + vec![format!("NBNS 2"), nbns2], + vec![format!("NBNS 3"), nbns3], + vec![format!("Domain"), domain], + vec![format!("Proxy PAC"), proxy_pac], + vec![format!("MTU"), mtu], + vec![format!("Gateway Address"), gateway_addr], + ]; + + rows.extend(info_rows); + } + + table.add_rows(rows); + + println!("{table}"); + } + _ => { + println!("Received unexpected response"); + } + } + } + } + Err(e) => { + eprintln!("Failed to connect to server: {}", e); + std::process::exit(1); + } + } + }); +} + +pub fn request_stop_server() { + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + + runtime.block_on(async { + let client = sock::UnixDomainClient::connect().await; + + match client { + Ok(mut client) => { + client + .send(JsonRequest::Stop) + .await + .expect("Failed to send stop command"); + + if let Ok(Some(response)) = client.framed_reader.try_next().await { + match response { + JsonResponse::StopResult { server_name } => { + println!("Stopped connection to server: {}", server_name) + } + _ => { + println!("Received unexpected response"); + } + } + } + } + Err(e) => { + eprintln!("Failed to connect to server: {}", e); + std::process::exit(1); + } + }; + }); +} From 5a4de93b0ec1a0b88dd5824d06e8521005a06461 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Mon, 15 Apr 2024 16:03:50 +0800 Subject: [PATCH 09/14] refactor: split cli unix c/s codes --- .../src/{ => client}/config.rs | 0 crates/openconnect-cli/src/client/mod.rs | 2 + .../openconnect-cli/src/{ => client}/state.rs | 0 crates/openconnect-cli/src/main.rs | 151 +++--------------- crates/openconnect-cli/src/server/mod.rs | 107 +++++++++++++ crates/openconnect-cli/src/sock.rs | 16 +- 6 files changed, 143 insertions(+), 133 deletions(-) rename crates/openconnect-cli/src/{ => client}/config.rs (100%) create mode 100644 crates/openconnect-cli/src/client/mod.rs rename crates/openconnect-cli/src/{ => client}/state.rs (100%) create mode 100644 crates/openconnect-cli/src/server/mod.rs diff --git a/crates/openconnect-cli/src/config.rs b/crates/openconnect-cli/src/client/config.rs similarity index 100% rename from crates/openconnect-cli/src/config.rs rename to crates/openconnect-cli/src/client/config.rs diff --git a/crates/openconnect-cli/src/client/mod.rs b/crates/openconnect-cli/src/client/mod.rs new file mode 100644 index 0000000..0f57072 --- /dev/null +++ b/crates/openconnect-cli/src/client/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod config; +pub(crate) mod state; diff --git a/crates/openconnect-cli/src/state.rs b/crates/openconnect-cli/src/client/state.rs similarity index 100% rename from crates/openconnect-cli/src/state.rs rename to crates/openconnect-cli/src/client/state.rs diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index 456ccf2..69694e7 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -1,24 +1,13 @@ mod cli; -mod config; +mod client; mod daemon; +mod server; mod sock; -mod state; -use crate::sock::UnixDomainServer; use clap::Parser; use cli::{Cli, Commands}; -use futures::{SinkExt, TryStreamExt}; -use openconnect_core::{ - ip_info::IpInfo, - log::Logger, - storage::{StoredConfigs, StoredServer}, - Connectable, Status, VpnClient, -}; -use std::{error::Error, io::BufRead, path::PathBuf, sync::Arc}; -use tokio::{ - select, - signal::unix::{signal, SignalKind}, -}; +use openconnect_core::{ip_info::IpInfo, log::Logger, storage::StoredConfigs}; +use std::{io::BufRead, path::PathBuf}; #[derive(serde::Serialize, serde::Deserialize)] pub enum JsonRequest { @@ -40,125 +29,32 @@ pub enum JsonResponse { }, } -async fn try_accept(listener: &tokio::net::UnixListener, client: Arc) { - if let Ok((stream, _)) = listener.accept().await { - let (read, write) = stream.into_split(); - let mut framed_reader = sock::get_framed_reader::(read); - let mut framed_writer = sock::get_framed_writer::(write); - - tokio::spawn(async move { - while let Ok(Some(command)) = framed_reader.try_next().await { - match command { - JsonRequest::Stop => { - let server_name = client.get_server_name().unwrap_or("".to_string()); - client.disconnect(); - - // ignore send error - let _ = framed_writer - .send(JsonResponse::StopResult { server_name }) - .await; - unsafe { - libc::raise(libc::SIGTERM); - } - } - - JsonRequest::Info => { - let server_name = client.get_server_name().unwrap_or("".to_string()); - let server_url = client.get_server_url().unwrap_or("".to_string()); - let hostname = client.get_hostname().unwrap_or("".to_string()); - let status = client.get_status(); - let info = client.get_info().ok().flatten().map(Box::new); - let status = match status { - Status::Connected => "Connected", - Status::Connecting(_) => "Connecting", - Status::Disconnected => "Disconnected", - Status::Disconnecting => "Disconnecting", - Status::Error(_) => "Error", - Status::Initialized => "Initialized", - } - .to_string(); - - // ignore send error - let _ = framed_writer - .send(JsonResponse::InfoResult { - server_name, - server_url, - hostname, - status, - info, - }) - .await; - } - } - } - }); - } -} - -async fn start_daemon( - stored_server: &StoredServer, - stored_configs: &StoredConfigs, -) -> Result<(), Box> { - let server = UnixDomainServer::bind()?; - let mut sigterm = signal(SignalKind::terminate())?; - let mut sigint = signal(SignalKind::interrupt())?; - let mut sigquit = signal(SignalKind::quit())?; - - let client = match stored_server { - StoredServer::Password(password_server) => { - crate::state::connect_password_server(password_server, stored_configs).await? - } - StoredServer::Oidc(_) => { - panic!("OIDC server not implemented"); - } - }; - - loop { - select! { - _ = sigquit.recv() => { - break; - } - _ = sigint.recv() => { - break; - } - _ = sigterm.recv() => { - break; - } - _ = try_accept(&server.listener, client.clone()) => { - // noop - } - }; - } - - Ok(()) -} - fn main() { let cli = Cli::parse(); match cli.command { Commands::Add(server_config) => { - crate::config::request_add_server(server_config); + crate::client::config::request_add_server(server_config); } Commands::Import { base64 } => { - crate::config::request_import_server(&base64); + crate::client::config::request_import_server(&base64); } Commands::Export { name } => { - crate::config::request_export_server(&name); + crate::client::config::request_export_server(&name); } Commands::Delete { name } => { - crate::config::request_delete_server(&name); + crate::client::config::request_delete_server(&name); } Commands::List => { - crate::config::request_list_servers(); + crate::client::config::request_list_servers(); } Commands::Status => { - crate::state::request_get_status(); + crate::client::state::request_get_status(); } Commands::Logs => { @@ -179,30 +75,25 @@ fn main() { } Commands::Stop => { - crate::state::request_stop_server(); + crate::client::state::request_stop_server(); } Commands::Start { name, config_file } => { - if sock::exists() { - eprintln!("Socket already exists. You may have a connected VPN session or a stale socket file. You may solve by:"); - eprintln!("1. Stopping the connection by sending stop command."); - eprintln!( - "2. Manually deleting the socket file which located at: {}", - sock::get_sock().display() - ); - std::process::exit(1); - } + sock::exit_when_socket_exists(); let config_file = config_file.map(PathBuf::from).unwrap_or( StoredConfigs::getorinit_config_file().expect("Failed to get config file"), ); sudo::escalate_if_needed().expect("Failed to escalate permissions"); + match daemon::daemonize() { daemon::ForkResult::Parent => { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); runtime.block_on(async { - match crate::config::read_server_config_from_fs(&name, config_file).await { + match crate::client::config::read_server_config_from_fs(&name, config_file) + .await + { Ok(_) => {} Err(e) => { eprintln!("Failed to get server: {}", e); @@ -224,16 +115,18 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); let (server, configs) = runtime.block_on(async { - crate::config::read_server_config_from_fs(&name, config_file) + crate::client::config::read_server_config_from_fs(&name, config_file) .await .expect("Failed to get server") }); runtime.block_on(async { Logger::init().expect("Failed to initialize logger"); - let _ = start_daemon(&server, &configs).await.inspect_err(|e| { - tracing::error!("Failed to start daemon: {}", e); - }); + let _ = crate::server::start_daemon(&server, &configs) + .await + .inspect_err(|e| { + tracing::error!("Failed to start daemon: {}", e); + }); }); } } diff --git a/crates/openconnect-cli/src/server/mod.rs b/crates/openconnect-cli/src/server/mod.rs new file mode 100644 index 0000000..bd2df07 --- /dev/null +++ b/crates/openconnect-cli/src/server/mod.rs @@ -0,0 +1,107 @@ +use crate::{ + sock::{self, UnixDomainServer}, + JsonRequest, JsonResponse, +}; +use futures::{SinkExt, TryStreamExt}; +use openconnect_core::{ + storage::{StoredConfigs, StoredServer}, + Connectable, Status, VpnClient, +}; +use std::{error::Error, sync::Arc}; +use tokio::{ + select, + signal::unix::{signal, SignalKind}, +}; + +pub async fn try_accept(listener: &tokio::net::UnixListener, client: Arc) { + if let Ok((stream, _)) = listener.accept().await { + let (read, write) = stream.into_split(); + let mut framed_reader = sock::get_framed_reader::(read); + let mut framed_writer = sock::get_framed_writer::(write); + + tokio::spawn(async move { + while let Ok(Some(command)) = framed_reader.try_next().await { + match command { + JsonRequest::Stop => { + let server_name = client.get_server_name().unwrap_or("".to_string()); + client.disconnect(); + + // ignore send error + let _ = framed_writer + .send(JsonResponse::StopResult { server_name }) + .await; + unsafe { + libc::raise(libc::SIGTERM); + } + } + + JsonRequest::Info => { + let server_name = client.get_server_name().unwrap_or("".to_string()); + let server_url = client.get_server_url().unwrap_or("".to_string()); + let hostname = client.get_hostname().unwrap_or("".to_string()); + let status = client.get_status(); + let info = client.get_info().ok().flatten().map(Box::new); + let status = match status { + Status::Connected => "Connected", + Status::Connecting(_) => "Connecting", + Status::Disconnected => "Disconnected", + Status::Disconnecting => "Disconnecting", + Status::Error(_) => "Error", + Status::Initialized => "Initialized", + } + .to_string(); + + // ignore send error + let _ = framed_writer + .send(JsonResponse::InfoResult { + server_name, + server_url, + hostname, + status, + info, + }) + .await; + } + } + } + }); + } +} + +pub async fn start_daemon( + stored_server: &StoredServer, + stored_configs: &StoredConfigs, +) -> Result<(), Box> { + let server = UnixDomainServer::bind()?; + let mut sigterm = signal(SignalKind::terminate())?; + let mut sigint = signal(SignalKind::interrupt())?; + let mut sigquit = signal(SignalKind::quit())?; + + let client = match stored_server { + StoredServer::Password(password_server) => { + crate::client::state::connect_password_server(password_server, stored_configs).await? + } + StoredServer::Oidc(_) => { + panic!("OIDC server not implemented"); + } + }; + + loop { + select! { + _ = sigquit.recv() => { + break; + } + _ = sigint.recv() => { + break; + } + _ = sigterm.recv() => { + break; + } + _ = try_accept(&server.listener, client.clone()) => { + // noop + } + }; + } + + Ok(()) +} diff --git a/crates/openconnect-cli/src/sock.rs b/crates/openconnect-cli/src/sock.rs index 2bfca35..4ed8ef7 100644 --- a/crates/openconnect-cli/src/sock.rs +++ b/crates/openconnect-cli/src/sock.rs @@ -23,6 +23,18 @@ pub fn get_sock() -> PathBuf { tmp.join("openconnect-rs.sock") } +pub fn exit_when_socket_exists() { + if get_sock().exists() { + eprintln!("Socket already exists. You may have a connected VPN session or a stale socket file. You may solve by:"); + eprintln!("1. Stopping the connection by sending stop command."); + eprintln!( + "2. Manually deleting the socket file which located at: {}", + get_sock().display() + ); + std::process::exit(1); + } +} + pub type FramedWriter = Framed, T, T, SymmetricalJson>; pub type FramedReader = @@ -40,10 +52,6 @@ pub fn get_framed_reader(read_half: OwnedReadHalf) -> FramedReader tokio_serde::SymmetricallyFramed::new(length_delimited, codec) } -pub fn exists() -> bool { - get_sock().exists() -} - pub struct UnixDomainServer { pub listener: UnixListener, } From 6af60289ad32cc6b2c99bc644936f9a15c5882a8 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Mon, 15 Apr 2024 16:57:05 +0800 Subject: [PATCH 10/14] feat: impl completion generator --- Cargo.lock | 10 ++++++ Cargo.toml | 1 + crates/openconnect-cli/Cargo.toml | 1 + crates/openconnect-cli/src/cli.rs | 39 +++++++++++++++++---- crates/openconnect-cli/src/client/config.rs | 6 +++- crates/openconnect-cli/src/main.rs | 3 ++ 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 742c094..9c2183e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,15 @@ dependencies = [ "strsim 0.11.1", ] +[[package]] +name = "clap_complete" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.4" @@ -2855,6 +2864,7 @@ dependencies = [ "base64 0.22.0", "chrono", "clap", + "clap_complete", "comfy-table", "dialoguer", "futures", diff --git a/Cargo.toml b/Cargo.toml index 3b8ba59..3937748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ reqwest = { version = "0.12.2", features = [ ] } base64 = "0.22.0" clap = { version = "4.5.4", features = ["derive"] } +clap_complete = "4.5.2" tokio = { version = "1.37.0", features = ["full"] } tokio-serde = { version = "0.9.0", features = ["json", "serde"] } tokio-util = { version = "0.7.10", features = ["codec"] } diff --git a/crates/openconnect-cli/Cargo.toml b/crates/openconnect-cli/Cargo.toml index 5e6e160..0340a02 100644 --- a/crates/openconnect-cli/Cargo.toml +++ b/crates/openconnect-cli/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] base64 = { workspace = true } clap = { workspace = true } +clap_complete = { workspace = true } tokio = { workspace = true } tokio-serde = { workspace = true } tokio-util = { workspace = true } diff --git a/crates/openconnect-cli/src/cli.rs b/crates/openconnect-cli/src/cli.rs index eed3fee..54ed136 100644 --- a/crates/openconnect-cli/src/cli.rs +++ b/crates/openconnect-cli/src/cli.rs @@ -1,10 +1,11 @@ -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; +use clap_complete::{generate, Shell}; #[derive(Parser, Debug)] #[clap( name = env!("CARGO_PKG_NAME"), version = env!("CARGO_PKG_VERSION"), - about = env!("CARGO_PKG_DESCRIPTION"), + long_about = env!("CARGO_PKG_DESCRIPTION"), )] pub struct Cli { #[command(subcommand)] @@ -59,46 +60,70 @@ pub enum Commands { #[command(about = "Show logs of the daemon process", visible_aliases = ["log"])] Logs, + + #[command(about = "Generate shell completion script")] + GenComplete { generator: Shell }, } #[derive(Subcommand, Debug)] pub enum SeverConfigArgs { - #[command(about = "Add an OIDC authentication VPN server")] + #[command(long_about = "Add an OIDC authentication VPN server")] Oidc { + /// The unique name of the VPN server configuration #[arg(short, long)] name: String, + /// The VPN server URL #[arg(short, long, value_hint = clap::ValueHint::Url)] server: String, + /// The OIDC issuer URL #[arg(short = 'I', long)] issuer: String, + /// The OIDC client ID #[arg(short = 'i', long)] client_id: String, + /// The OIDC client secret #[arg(short = 'k', long)] client_secret: Option, + /// Allow insecure peer certificate verification #[arg(short, long, default_value = "false")] allow_insecure: Option, }, - #[command(about = "Add a password authentication VPN server")] + #[command( + long_about = "Add a password authentication VPN server. For safty reason, the password input will be prompted in terminal later" + )] Password { + /// The unique name of the VPN server configuration #[arg(short, long)] name: String, + /// The VPN server URL #[arg(short, long, value_hint = clap::ValueHint::Url)] server: String, + /// The username for password authentication #[arg(short, long)] username: String, - #[arg(short, long)] - password: String, - + /// Allow insecure peer certificate verification #[arg(short, long, default_value = "false")] allow_insecure: Option, }, } + +pub fn print_completions(generator: Shell) { + let mut cmd = Cli::command(); + let cmd = &mut cmd; + + generate( + generator, + cmd, + cmd.get_name().to_string(), + &mut std::io::stdout(), + ); +} diff --git a/crates/openconnect-cli/src/client/config.rs b/crates/openconnect-cli/src/client/config.rs index 3a38852..c501c1c 100644 --- a/crates/openconnect-cli/src/client/config.rs +++ b/crates/openconnect-cli/src/client/config.rs @@ -78,9 +78,13 @@ pub fn request_add_server(server_config: SeverConfigArgs) { name, server, username, - password, allow_insecure, } => { + let password = dialoguer::Password::new() + .with_prompt("Enter password") + .interact() + .expect("Failed to get password"); + let password_server = PasswordServer { name, server, diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index 69694e7..b59da28 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -33,6 +33,9 @@ fn main() { let cli = Cli::parse(); match cli.command { + Commands::GenComplete { generator } => { + cli::print_completions(generator); + } Commands::Add(server_config) => { crate::client::config::request_add_server(server_config); } From f9dab994e30f2edc6d97bf14d269e884c4ab985d Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Wed, 17 Apr 2024 12:30:02 +0800 Subject: [PATCH 11/14] refactor: split oidc from gui, preparing for device authentication on console --- Cargo.lock | 14 ++ Cargo.toml | 4 +- crates/openconnect-cli/Cargo.toml | 1 + crates/openconnect-cli/src/client/config.rs | 4 +- crates/openconnect-cli/src/client/state.rs | 37 +++- crates/openconnect-cli/src/main.rs | 59 +++++- crates/openconnect-cli/src/server/mod.rs | 191 ++++++++++++------ crates/openconnect-core/src/command.rs | 2 - crates/openconnect-core/src/lib.rs | 11 +- crates/openconnect-gui/src-tauri/Cargo.toml | 1 + crates/openconnect-gui/src-tauri/src/main.rs | 1 - crates/openconnect-gui/src-tauri/src/state.rs | 20 +- crates/openconnect-oidc/Cargo.toml | 15 ++ crates/openconnect-oidc/src/lib.rs | 50 +++++ crates/openconnect-oidc/src/oidc_device.rs | 151 ++++++++++++++ crates/openconnect-oidc/src/oidc_token.rs | 159 +++++++++++++++ 16 files changed, 614 insertions(+), 106 deletions(-) create mode 100644 crates/openconnect-oidc/Cargo.toml create mode 100644 crates/openconnect-oidc/src/lib.rs create mode 100644 crates/openconnect-oidc/src/oidc_device.rs create mode 100644 crates/openconnect-oidc/src/oidc_token.rs diff --git a/Cargo.lock b/Cargo.lock index 9c2183e..2dfe4fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2861,6 +2861,7 @@ dependencies = [ name = "openconnect-cli" version = "0.1.1" dependencies = [ + "anyhow", "base64 0.22.0", "chrono", "clap", @@ -2923,6 +2924,7 @@ dependencies = [ "libc", "open 5.1.2", "openconnect-core", + "openconnect-oidc", "openidconnect", "reqwest 0.12.3", "serde", @@ -2937,6 +2939,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "openconnect-oidc" +version = "0.1.0" +dependencies = [ + "openidconnect", + "reqwest 0.12.3", + "serde", + "thiserror", + "tokio", + "url", +] + [[package]] name = "openconnect-sys" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 3937748..3b058cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ "crates/openconnect-sys", "crates/openconnect-core", "crates/openconnect-gui/src-tauri", - "crates/openconnect-cli", + "crates/openconnect-cli", "crates/openconnect-oidc", ] [workspace.dependencies] @@ -48,4 +48,4 @@ machine-uid = "0.5.1" chacha20poly1305 = "0.10.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" -tracing-appender = "0.2.3" \ No newline at end of file +tracing-appender = "0.2.3" diff --git a/crates/openconnect-cli/Cargo.toml b/crates/openconnect-cli/Cargo.toml index 0340a02..56cdaae 100644 --- a/crates/openconnect-cli/Cargo.toml +++ b/crates/openconnect-cli/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.1" edition = "2021" [dependencies] +anyhow = { workspace = true } base64 = { workspace = true } clap = { workspace = true } clap_complete = { workspace = true } diff --git a/crates/openconnect-cli/src/client/config.rs b/crates/openconnect-cli/src/client/config.rs index c501c1c..fd15772 100644 --- a/crates/openconnect-cli/src/client/config.rs +++ b/crates/openconnect-cli/src/client/config.rs @@ -2,12 +2,12 @@ use crate::cli::SeverConfigArgs; use base64::Engine; use comfy_table::Table; use openconnect_core::storage::{OidcServer, PasswordServer, StoredConfigs, StoredServer}; -use std::{error::Error, path::PathBuf}; +use std::path::PathBuf; pub async fn read_server_config_from_fs( server_name: &str, config_file: PathBuf, -) -> Result<(StoredServer, StoredConfigs), Box> { +) -> anyhow::Result<(StoredServer, StoredConfigs)> { let mut stored_configs = StoredConfigs::new(None, config_file); let config = stored_configs.read_from_file().await?; let server = config.servers.get(server_name); diff --git a/crates/openconnect-cli/src/client/state.rs b/crates/openconnect-cli/src/client/state.rs index 5385afe..3a45f66 100644 --- a/crates/openconnect-cli/src/client/state.rs +++ b/crates/openconnect-cli/src/client/state.rs @@ -7,19 +7,31 @@ use openconnect_core::{ storage::{PasswordServer, StoredConfigs}, Connectable, VpnClient, }; -use std::{error::Error, sync::Arc}; -pub async fn connect_password_server( +pub fn get_vpnc_script() -> anyhow::Result { + let homedir = home::home_dir().ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to get home directory", + ))?; + let vpncscript = homedir.join(".oidcvpn/bin/vpnc-script"); + let vpncscript = vpncscript.to_str().ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to get vpnc-script path as string", + ))?; + + Ok(vpncscript.to_string()) +} + +pub async fn obtain_cookie_from_password_server( password_server: &PasswordServer, stored_configs: &StoredConfigs, -) -> Result, Box> { +) -> anyhow::Result> { let password_server = password_server.decrypted_by(&stored_configs.cipher); - let homedir = home::home_dir().ok_or("Failed to get home directory")?; - let vpncscript = homedir.join(".oidcvpn/bin/vpnc-script"); - let vpncscript = vpncscript.to_str().ok_or("Failed to get vpncscript path")?; + + let vpncscript = get_vpnc_script()?; let config = ConfigBuilder::default() - .vpncscript(vpncscript) + .vpncscript(&vpncscript) .loglevel(LogLevel::Info) .build()?; @@ -37,11 +49,14 @@ pub async fn connect_password_server( let client = VpnClient::new(config, event_handler)?; let client_clone = client.clone(); - tokio::task::spawn_blocking(move || { - let _ = client_clone.connect(entrypoint); - }); + let result = + tokio::task::spawn_blocking(move || client_clone.connect_for_cookie(entrypoint)).await; - Ok(client) + match result { + Ok(Ok(cookie)) => Ok(cookie), + Ok(Err(e)) => Err(e.into()), + Err(e) => Err(e.into()), + } } pub fn request_get_status() { diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index b59da28..c5c48f8 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -6,17 +6,32 @@ mod sock; use clap::Parser; use cli::{Cli, Commands}; -use openconnect_core::{ip_info::IpInfo, log::Logger, storage::StoredConfigs}; +use openconnect_core::{ + ip_info::IpInfo, + log::Logger, + storage::{StoredConfigs, StoredServer}, +}; use std::{io::BufRead, path::PathBuf}; +use crate::sock::UnixDomainClient; + #[derive(serde::Serialize, serde::Deserialize)] pub enum JsonRequest { + Start { + name: String, + server: String, + allow_insecure: bool, + cookie: String, + }, Stop, Info, } #[derive(serde::Serialize, serde::Deserialize)] pub enum JsonResponse { + StartResult { + server_name: String, + }, StopResult { server_name: String, }, @@ -34,7 +49,7 @@ fn main() { match cli.command { Commands::GenComplete { generator } => { - cli::print_completions(generator); + crate::cli::print_completions(generator); } Commands::Add(server_config) => { crate::client::config::request_add_server(server_config); @@ -97,7 +112,31 @@ fn main() { match crate::client::config::read_server_config_from_fs(&name, config_file) .await { - Ok(_) => {} + Ok((stored_server, stored_configs)) => { + let (cookie, name, server, allow_insecure) = match stored_server { + StoredServer::Password(password_server) => { + let cookie = crate::client::state::obtain_cookie_from_password_server( + &password_server, + &stored_configs, + ) + .await.unwrap(); + (cookie, password_server.name, password_server.server, password_server.allow_insecure) + } + StoredServer::Oidc(_) => { + todo!("OIDC server not implemented"); + } + }; + + if let Some(cookie) = cookie { + let mut unix_client = UnixDomainClient::connect().await.unwrap(); + let _ = unix_client.send(JsonRequest::Start { + name, + server, + allow_insecure: allow_insecure.unwrap_or(false), + cookie, + }).await; + } + } Err(e) => { eprintln!("Failed to get server: {}", e); std::process::exit(1); @@ -117,19 +156,17 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - let (server, configs) = runtime.block_on(async { + runtime.block_on(async { crate::client::config::read_server_config_from_fs(&name, config_file) - .await - .expect("Failed to get server") + .await + .expect("Failed to get server"); }); runtime.block_on(async { Logger::init().expect("Failed to initialize logger"); - let _ = crate::server::start_daemon(&server, &configs) - .await - .inspect_err(|e| { - tracing::error!("Failed to start daemon: {}", e); - }); + let _ = crate::server::start_daemon().await.inspect_err(|e| { + tracing::error!("Failed to start daemon: {}", e); + }); }); } } diff --git a/crates/openconnect-cli/src/server/mod.rs b/crates/openconnect-cli/src/server/mod.rs index bd2df07..a33b358 100644 --- a/crates/openconnect-cli/src/server/mod.rs +++ b/crates/openconnect-cli/src/server/mod.rs @@ -1,92 +1,157 @@ use crate::{ + client::state::get_vpnc_script, sock::{self, UnixDomainServer}, JsonRequest, JsonResponse, }; use futures::{SinkExt, TryStreamExt}; use openconnect_core::{ - storage::{StoredConfigs, StoredServer}, + config::{ConfigBuilder, EntrypointBuilder, LogLevel}, + events::EventHandlers, Connectable, Status, VpnClient, }; -use std::{error::Error, sync::Arc}; +use std::sync::Arc; use tokio::{ select, signal::unix::{signal, SignalKind}, + sync::RwLock, }; -pub async fn try_accept(listener: &tokio::net::UnixListener, client: Arc) { - if let Ok((stream, _)) = listener.accept().await { - let (read, write) = stream.into_split(); - let mut framed_reader = sock::get_framed_reader::(read); - let mut framed_writer = sock::get_framed_writer::(write); - - tokio::spawn(async move { - while let Ok(Some(command)) = framed_reader.try_next().await { - match command { - JsonRequest::Stop => { - let server_name = client.get_server_name().unwrap_or("".to_string()); - client.disconnect(); - - // ignore send error - let _ = framed_writer - .send(JsonResponse::StopResult { server_name }) - .await; - unsafe { - libc::raise(libc::SIGTERM); +struct State { + client: RwLock>>, + server: UnixDomainServer, +} + +impl State { + pub fn new(server: UnixDomainServer) -> Arc { + Arc::new(State { + client: RwLock::new(None), + server, + }) + } +} + +trait Acceptable { + async fn try_accept(self); +} + +impl Acceptable for Arc { + async fn try_accept(self) { + if let Ok((stream, _)) = self.server.listener.accept().await { + let (read, write) = stream.into_split(); + let mut framed_reader = sock::get_framed_reader::(read); + let mut framed_writer = sock::get_framed_writer::(write); + + tokio::spawn(async move { + while let Ok(Some(command)) = framed_reader.try_next().await { + match command { + JsonRequest::Start { + name: server_name, + server: server_url, + allow_insecure, + cookie, + } => { + let vpncscript = get_vpnc_script().unwrap(); + + let config = ConfigBuilder::default() + .vpncscript(&vpncscript) + .loglevel(LogLevel::Info) + .build() + .unwrap(); + + let entrypoint = EntrypointBuilder::new() + .name(&server_name) + .server(&server_url) + .accept_insecure_cert(allow_insecure) + .cookie(&cookie) + .enable_udp(true) + .build() + .unwrap(); + + let event_handler = EventHandlers::default(); + + let client = VpnClient::new(config, event_handler).unwrap(); + let client_cloned = client.clone(); + + tokio::task::spawn_blocking(move || { + let _ = client.connect(entrypoint); + }); + + { + let mut client_to_write = self.client.write().await; + *client_to_write = Some(client_cloned); + } + } + + JsonRequest::Stop => { + { + let client = self.client.read().await; + if let Some(ref client) = *client { + let server_name = + client.get_server_name().unwrap_or("".to_string()); + client.disconnect(); + + // ignore send error + let _ = framed_writer + .send(JsonResponse::StopResult { server_name }) + .await; + unsafe { + libc::raise(libc::SIGTERM); + } + } + self.client.write().await.take(); + } } - } - JsonRequest::Info => { - let server_name = client.get_server_name().unwrap_or("".to_string()); - let server_url = client.get_server_url().unwrap_or("".to_string()); - let hostname = client.get_hostname().unwrap_or("".to_string()); - let status = client.get_status(); - let info = client.get_info().ok().flatten().map(Box::new); - let status = match status { - Status::Connected => "Connected", - Status::Connecting(_) => "Connecting", - Status::Disconnected => "Disconnected", - Status::Disconnecting => "Disconnecting", - Status::Error(_) => "Error", - Status::Initialized => "Initialized", + JsonRequest::Info => { + { + let client = self.client.read().await; + if let Some(client) = (*client).clone() { + let server_name = + client.get_server_name().unwrap_or("".to_string()); + let server_url = + client.get_server_url().unwrap_or("".to_string()); + let hostname = client.get_hostname().unwrap_or("".to_string()); + let status = client.get_status(); + let info = client.get_info().ok().flatten().map(Box::new); + let status = match status { + Status::Connected => "Connected", + Status::Connecting(_) => "Connecting", + Status::Disconnected => "Disconnected", + Status::Disconnecting => "Disconnecting", + Status::Error(_) => "Error", + Status::Initialized => "Initialized", + } + .to_string(); + + // ignore send error + let _ = framed_writer + .send(JsonResponse::InfoResult { + server_name, + server_url, + hostname, + status, + info, + }) + .await; + } + } } - .to_string(); - - // ignore send error - let _ = framed_writer - .send(JsonResponse::InfoResult { - server_name, - server_url, - hostname, - status, - info, - }) - .await; } } - } - }); + }); + } } } -pub async fn start_daemon( - stored_server: &StoredServer, - stored_configs: &StoredConfigs, -) -> Result<(), Box> { +pub async fn start_daemon() -> anyhow::Result<()> { let server = UnixDomainServer::bind()?; let mut sigterm = signal(SignalKind::terminate())?; let mut sigint = signal(SignalKind::interrupt())?; let mut sigquit = signal(SignalKind::quit())?; - - let client = match stored_server { - StoredServer::Password(password_server) => { - crate::client::state::connect_password_server(password_server, stored_configs).await? - } - StoredServer::Oidc(_) => { - panic!("OIDC server not implemented"); - } - }; + let state = State::new(server); loop { + let state = state.clone(); select! { _ = sigquit.recv() => { break; @@ -97,7 +162,7 @@ pub async fn start_daemon( _ = sigterm.recv() => { break; } - _ = try_accept(&server.listener, client.clone()) => { + _ = state.try_accept() => { // noop } }; diff --git a/crates/openconnect-core/src/command.rs b/crates/openconnect-core/src/command.rs index ef0ea5d..ae43197 100644 --- a/crates/openconnect-core/src/command.rs +++ b/crates/openconnect-core/src/command.rs @@ -127,8 +127,6 @@ impl SignalHandle { .expect("Failed to register signal handler"); std::thread::spawn(move || { - println!("Signal handler thread started"); - for sig in signals.forever() { let cmd = match sig { SIGINT | SIGTERM => { diff --git a/crates/openconnect-core/src/lib.rs b/crates/openconnect-core/src/lib.rs index aa54c5c..15a0c76 100644 --- a/crates/openconnect-core/src/lib.rs +++ b/crates/openconnect-core/src/lib.rs @@ -211,9 +211,6 @@ impl VpnClient { pub fn obtain_cookie(&self) -> OpenconnectResult<()> { let ret = unsafe { - println!(); - println!("obtain_cookie"); - println!(); openconnect_obtain_cookie(self.vpninfo) }; match ret { @@ -428,6 +425,7 @@ impl Drop for VpnClient { pub trait Connectable { fn new(config: Config, callbacks: EventHandlers) -> OpenconnectResult>; fn connect(&self, entrypoint: Entrypoint) -> OpenconnectResult<()>; + fn connect_for_cookie(&self, entrypoint: Entrypoint) -> OpenconnectResult>; fn disconnect(&self); fn get_status(&self) -> Status; fn get_server_name(&self) -> Option; @@ -491,7 +489,7 @@ impl Connectable for VpnClient { Ok(instance) } - fn connect(&self, entrypoint: Entrypoint) -> OpenconnectResult<()> { + fn connect_for_cookie(&self, entrypoint: Entrypoint) -> OpenconnectResult> { self.emit_state_change(Status::Connecting("Initializing connection".to_string())); { if let Ok(mut form_context) = self.form_manager.try_write() { @@ -548,7 +546,12 @@ impl Connectable for VpnClient { self.obtain_cookie().emit_error(self)?; } + Ok(self.get_cookie()) + } + + fn connect(&self, entrypoint: Entrypoint) -> OpenconnectResult<()> { self.emit_state_change(Status::Connecting("Make CSTP connection".to_string())); + self.connect_for_cookie(entrypoint)?; self.make_cstp_connection().emit_error(self)?; self.emit_state_change(Status::Connected); diff --git a/crates/openconnect-gui/src-tauri/Cargo.toml b/crates/openconnect-gui/src-tauri/Cargo.toml index 77e841b..4582499 100644 --- a/crates/openconnect-gui/src-tauri/Cargo.toml +++ b/crates/openconnect-gui/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ dotenvy = { workspace = true } lazy_static = { workspace = true } open = "5.1.2" openconnect-core = { path = "../../openconnect-core" } +openconnect-oidc = { path = "../../openconnect-oidc" } openidconnect = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/openconnect-gui/src-tauri/src/main.rs b/crates/openconnect-gui/src-tauri/src/main.rs index 96c0f1c..cef3e79 100644 --- a/crates/openconnect-gui/src-tauri/src/main.rs +++ b/crates/openconnect-gui/src-tauri/src/main.rs @@ -2,7 +2,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod command; -mod oidc; mod state; mod system_tray; diff --git a/crates/openconnect-gui/src-tauri/src/state.rs b/crates/openconnect-gui/src-tauri/src/state.rs index 4823c72..e228073 100644 --- a/crates/openconnect-gui/src-tauri/src/state.rs +++ b/crates/openconnect-gui/src-tauri/src/state.rs @@ -1,13 +1,14 @@ -use crate::{ - oidc::{OpenID, OpenIDConfig, OpenIDError, OIDC_REDIRECT_URI}, - system_tray::AppSystemTray, -}; +use crate::system_tray::AppSystemTray; use openconnect_core::{ config::{ConfigBuilder, EntrypointBuilder, LogLevel}, events::EventHandlers, storage::{StoredConfigError, StoredConfigs, StoredServer}, Connectable, Status, VpnClient, }; +use openconnect_oidc::{ + obtain_cookie_by_oidc_token, + oidc_token::{OpenIDTokenAuth, OpenIDTokenAuthConfig, OpenIDTokenAuthError, OIDC_REDIRECT_URI}, +}; use std::{path::PathBuf, sync::Arc}; use tauri::{ async_runtime::{channel, RwLock, Sender}, @@ -31,7 +32,7 @@ pub enum StateError { TauriError(#[from] tauri::Error), #[error("OpenID error: {0}")] - OpenIdError(#[from] crate::oidc::OpenIDError), + OpenIdError(#[from] OpenIDTokenAuthError), #[error("IO error: {0}")] IoError(#[from] std::io::Error), @@ -177,7 +178,7 @@ impl AppState { let stored_server = self.stored_configs.read().await; let oidc_server = stored_server.get_server_as_oidc_server(server_name)?; - let openid_config = OpenIDConfig { + let openid_config = OpenIDTokenAuthConfig { issuer_url: oidc_server.issuer.clone(), redirect_uri: OIDC_REDIRECT_URI.to_string(), client_id: oidc_server.client_id.clone(), @@ -185,21 +186,20 @@ impl AppState { use_pkce_challenge: true, }; - let mut openid = OpenID::new(openid_config).await?; + let mut openid = OpenIDTokenAuth::new(openid_config).await?; let (authorize_url, req_state, _) = openid.auth_request(); open::that(authorize_url.to_string())?; let (code, callback_state) = openid.wait_for_callback().await?; if req_state.secret() != callback_state.secret() { - return Err(OpenIDError::StateValidationError( + return Err(OpenIDTokenAuthError::StateValidationError( "State validation failed".to_string(), ))?; } let token = openid.exchange_token(code).await?; - let cookie = openid - .obtain_cookie_by_oidc(&oidc_server.server, &token) + let cookie = obtain_cookie_by_oidc_token(&oidc_server.server, &token) .await .ok_or(StateError::IoError(std::io::Error::new( std::io::ErrorKind::InvalidData, diff --git a/crates/openconnect-oidc/Cargo.toml b/crates/openconnect-oidc/Cargo.toml new file mode 100644 index 0000000..10480f0 --- /dev/null +++ b/crates/openconnect-oidc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "openconnect-oidc" +version = "0.1.0" +edition = "2021" + +[dependencies] +openidconnect = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +reqwest = { workspace = true } + +[lib] +crate-type = ["lib"] \ No newline at end of file diff --git a/crates/openconnect-oidc/src/lib.rs b/crates/openconnect-oidc/src/lib.rs new file mode 100644 index 0000000..cf74b6e --- /dev/null +++ b/crates/openconnect-oidc/src/lib.rs @@ -0,0 +1,50 @@ +use std::str::FromStr; + +pub mod oidc_device; +pub mod oidc_token; + +pub async fn obtain_cookie_by_oidc_token(server_url: &str, token: &str) -> Option { + let client = reqwest::ClientBuilder::new() + .danger_accept_invalid_certs(true) + .default_headers(reqwest::header::HeaderMap::new()) + .http1_allow_obsolete_multiline_headers_in_responses(true) + .http1_allow_spaces_after_header_name_in_responses(true) + .http1_ignore_invalid_headers_in_responses(true) + .http1_title_case_headers() + .no_brotli() + .no_deflate() + .no_gzip() + .no_proxy() + .build() + .ok()?; + + let mut url = reqwest::Url::from_str(server_url).ok()?; + if url.path().is_empty() { + url.set_path("auth") + } + let req_builder = client + .post(url) + .header("Accept", "*/*") + .header("User-Agent", "AnnyConnect Compatible Client") + .header("Content-Type", "application/x-www-form-urlencoded") + .bearer_auth(token); + + let req = req_builder.build().ok()?; + + let res = client.execute(req).await.ok()?; + if !res.status().is_success() { + eprintln!( + "Failed to obtain cookie from server. Error code: {:?}", + res.status() + ); + return None; + } + + let combined_cookie = res + .cookies() + .map(|c| format!("{}={}", c.name(), c.value())) + .collect::>() + .join("; "); + + Some(combined_cookie) +} \ No newline at end of file diff --git a/crates/openconnect-oidc/src/oidc_device.rs b/crates/openconnect-oidc/src/oidc_device.rs new file mode 100644 index 0000000..4868c91 --- /dev/null +++ b/crates/openconnect-oidc/src/oidc_device.rs @@ -0,0 +1,151 @@ +use openidconnect::{ + core::{ + CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod, + CoreDeviceAuthorizationResponse, CoreGrantType, CoreJsonWebKey, CoreJsonWebKeyType, + CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, + CoreJwsSigningAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, + }, + reqwest::async_http_client, + AdditionalProviderMetadata, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, + IssuerUrl, ProviderMetadata, RedirectUrl, TokenResponse, +}; +use std::{future::Future, time::Duration}; + +pub struct OpenIDDeviceAuth { + client: CoreClient, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct DeviceEndpointProviderMetadata { + device_authorization_endpoint: DeviceAuthorizationUrl, +} + +impl AdditionalProviderMetadata for DeviceEndpointProviderMetadata {} + +type DeviceProviderMetadata = ProviderMetadata< + DeviceEndpointProviderMetadata, + CoreAuthDisplay, + CoreClientAuthMethod, + CoreClaimName, + CoreClaimType, + CoreGrantType, + CoreJweContentEncryptionAlgorithm, + CoreJweKeyManagementAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, + CoreResponseMode, + CoreResponseType, + CoreSubjectIdentifierType, +>; + +pub struct OpenIDDeviceAuthConfig { + pub issuer_url: String, + pub redirect_uri: Option, + pub client_id: String, + pub client_secret: Option, +} + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum OpenIDDeviceAuthError { + #[error("Failed to initialize OpenID client: {0}")] + InitError(String), + + #[error("Failed to exchange device token: {0}")] + ExchangeDeviceTokenError(String), + + #[error("URL parse error: {0}")] + UrlParseError(#[from] url::ParseError), + + #[error("Token exchange error: {0}")] + TokenExchangeError(String), +} + +impl OpenIDDeviceAuth { + pub async fn new(config: OpenIDDeviceAuthConfig) -> Result { + let issuer_url = IssuerUrl::new(config.issuer_url)?; + + let provider_metadata = + DeviceProviderMetadata::discover_async(issuer_url, async_http_client) + .await + .map_err(|e| OpenIDDeviceAuthError::InitError(e.to_string()))?; + + let device_authorization_endpoint = provider_metadata + .additional_metadata() + .device_authorization_endpoint + .clone(); + + let redirect_uri = config.redirect_uri.and_then(|x| RedirectUrl::new(x).ok()); + let client_id = ClientId::new(config.client_id); + let client_secret = config.client_secret.map(ClientSecret::new); + + let mut client = + CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret) + .set_device_authorization_uri(device_authorization_endpoint) + .set_auth_type(AuthType::RequestBody); + + if let Some(redirect_uri) = redirect_uri { + client = client.set_redirect_uri(redirect_uri); + } + + Ok(OpenIDDeviceAuth { client }) + } + + /// Exchange device token + /// + /// Example: + /// ```rust + /// let device_auth_response = openid_device_auth.exchange_device_token().await.unwrap(); + /// let verification_url = device_auth_response.verification_uri(); + /// let user_code = device_auth_response.user_code(); + /// println!( + /// "Please visit {} and enter code {}", + /// **verification_url, + /// . user_code.secret() + /// ); + /// ``` + pub async fn exchange_device_token( + &self, + ) -> Result { + let device_auth_request = self + .client + .exchange_device_code() + .map_err(|e| OpenIDDeviceAuthError::ExchangeDeviceTokenError(e.to_string()))?; + + let device_auth_response: CoreDeviceAuthorizationResponse = device_auth_request + .request_async(async_http_client) + .await + .map_err(|e| OpenIDDeviceAuthError::ExchangeDeviceTokenError(e.to_string()))?; + + Ok(device_auth_response) + } + + pub async fn exchange_token( + &mut self, + device_auth_response: &CoreDeviceAuthorizationResponse, + sleep_fn: S, + timout: Option, + ) -> Result + where + SF: Future, + S: Fn(Duration) -> SF, + { + let token_response = self + .client + .exchange_device_access_token(device_auth_response) + .request_async(async_http_client, sleep_fn, timout) + .await + .map_err(|e| OpenIDDeviceAuthError::TokenExchangeError(e.to_string()))?; + + let token = token_response + .id_token() + .ok_or(OpenIDDeviceAuthError::TokenExchangeError( + "No ID token".to_string(), + ))? + .to_string(); + + Ok(token) + } +} diff --git a/crates/openconnect-oidc/src/oidc_token.rs b/crates/openconnect-oidc/src/oidc_token.rs new file mode 100644 index 0000000..ff0279d --- /dev/null +++ b/crates/openconnect-oidc/src/oidc_token.rs @@ -0,0 +1,159 @@ +use openidconnect::{ + core::{CoreClient, CoreProviderMetadata, CoreResponseType}, + reqwest::async_http_client, + AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, + PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse, +}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; +use url::Url; + +pub const OIDC_LOCAL_PORT: u16 = 17175; +pub const OIDC_REDIRECT_URI: &str = "http://localhost:17175/callback"; + +pub struct OpenIDTokenAuth { + client: CoreClient, + pkce_challenge: Option, + pkce_verifier: Option, +} + +pub struct OpenIDTokenAuthConfig { + pub issuer_url: String, + pub redirect_uri: String, + pub client_id: String, + pub use_pkce_challenge: bool, + pub client_secret: Option, +} + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum OpenIDTokenAuthError { + #[error("Failed to initialize OpenID client: {0}")] + InitError(String), + + #[error("IO error: {0}")] + IoError(#[from] tokio::io::Error), + + #[error("URL parse error: {0}")] + UrlParseError(#[from] url::ParseError), + + #[error("OpenID state validation error: {0}")] + StateValidationError(String), + + #[error("Token exchange error: {0}")] + TokenExchangeError(String), +} + +impl OpenIDTokenAuth { + pub async fn new(config: OpenIDTokenAuthConfig) -> Result { + let issuer_url = IssuerUrl::new(config.issuer_url)?; + let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client) + .await + .map_err(|e| OpenIDTokenAuthError::InitError(e.to_string()))?; + let redirect_uri = RedirectUrl::new(config.redirect_uri)?; + let client_id = ClientId::new(config.client_id); + let client_secret = config.client_secret.map(ClientSecret::new); + + let client = + CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret) + .set_redirect_uri(redirect_uri); + + if config.use_pkce_challenge { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + Ok(OpenIDTokenAuth { + client, + pkce_challenge: Some(pkce_challenge), + pkce_verifier: Some(pkce_verifier), + }) + } else { + Ok(OpenIDTokenAuth { + client, + pkce_challenge: None, + pkce_verifier: None, + }) + } + } + + pub fn auth_request(&mut self) -> (Url, CsrfToken, Nonce) { + let mut auth_request = self.client.authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ); + + if let Some(pkce_challenge) = self.pkce_challenge.take() { + auth_request = auth_request.set_pkce_challenge(pkce_challenge); + } + + auth_request.url() + } + + pub fn parse_code_and_state(&self, url: Url) -> Option<(AuthorizationCode, CsrfToken)> { + let code = url + .query_pairs() + .find(|(key, _)| key == "code") + .map(|(_, code)| AuthorizationCode::new(code.into_owned()))?; + + let state = url + .query_pairs() + .find(|(key, _)| key == "state") + .map(|(_, state)| CsrfToken::new(state.into_owned()))?; + + Some((code, state)) + } + + pub async fn exchange_token(&mut self, code: AuthorizationCode) -> Result { + let client = self.client.clone(); + let pkce_verifier = self.pkce_verifier.take(); + + let mut token_response = client.exchange_code(code); + + if let Some(pkce_verifier) = pkce_verifier { + token_response = token_response.set_pkce_verifier(pkce_verifier); + } + + let token_response = token_response + .request_async(async_http_client) + .await + .map_err(|e| OpenIDTokenAuthError::TokenExchangeError(e.to_string()))?; + + let token = token_response + .id_token() + .ok_or(OpenIDTokenAuthError::TokenExchangeError("No ID token".to_string()))? + .to_string(); + + Ok(token) + } + + pub async fn wait_for_callback(&self) -> Result<(AuthorizationCode, CsrfToken), OpenIDTokenAuthError> { + let listener = + tokio::net::TcpListener::bind(format!("127.0.0.1:{}", OIDC_LOCAL_PORT)).await?; + let (mut stream, _) = listener.accept().await?; + let mut reader = tokio::io::BufReader::new(&mut stream); + let mut request_line = String::new(); + reader.read_line(&mut request_line).await?; + let redirect_url = request_line + .split_whitespace() + .nth(1) + .ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid request", + ))?; + let url = Url::parse(&format!("{},{}", OIDC_REDIRECT_URI, redirect_url))?; + + let (code, state) = self.parse_code_and_state(url).ok_or(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidInput, + "Failed to parse code and state", + ))?; + let message = "Authenticated, close this window and return to the application."; + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", + message.len(), + message + ); + + stream.write_all(response.as_bytes()).await?; + + Ok((code, state)) + } +} From 2252501a1546cea62bcb525bae3b0adef50c3f8b Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Wed, 17 Apr 2024 13:59:11 +0800 Subject: [PATCH 12/14] refactor: cli readable error and result --- crates/openconnect-cli/src/client/state.rs | 109 ++++++++++++++++-- crates/openconnect-cli/src/main.rs | 57 +-------- crates/openconnect-cli/src/server/mod.rs | 97 ++++++++++------ crates/openconnect-gui/src-tauri/src/state.rs | 32 ++--- 4 files changed, 174 insertions(+), 121 deletions(-) diff --git a/crates/openconnect-cli/src/client/state.rs b/crates/openconnect-cli/src/client/state.rs index 3a45f66..8ef0a82 100644 --- a/crates/openconnect-cli/src/client/state.rs +++ b/crates/openconnect-cli/src/client/state.rs @@ -1,14 +1,30 @@ +use std::path::PathBuf; + use crate::{sock, JsonRequest, JsonResponse}; use comfy_table::Table; use futures::TryStreamExt; use openconnect_core::{ config::{ConfigBuilder, EntrypointBuilder, LogLevel}, events::EventHandlers, - storage::{PasswordServer, StoredConfigs}, + result::OpenconnectError, + storage::{PasswordServer, StoredConfigs, StoredServer}, Connectable, VpnClient, }; -pub fn get_vpnc_script() -> anyhow::Result { +#[allow(clippy::enum_variant_names)] +#[derive(thiserror::Error, Debug)] +pub enum StateError { + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Openconnect error: {0}")] + OpenconnectError(#[from] OpenconnectError), + + #[error("Tokio task error: {0}")] + TokioTaskError(#[from] tokio::task::JoinError), +} + +pub fn get_vpnc_script() -> Result { let homedir = home::home_dir().ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "Failed to get home directory", @@ -25,7 +41,7 @@ pub fn get_vpnc_script() -> anyhow::Result { pub async fn obtain_cookie_from_password_server( password_server: &PasswordServer, stored_configs: &StoredConfigs, -) -> anyhow::Result> { +) -> Result, StateError> { let password_server = password_server.decrypted_by(&stored_configs.cipher); let vpncscript = get_vpnc_script()?; @@ -49,14 +65,7 @@ pub async fn obtain_cookie_from_password_server( let client = VpnClient::new(config, event_handler)?; let client_clone = client.clone(); - let result = - tokio::task::spawn_blocking(move || client_clone.connect_for_cookie(entrypoint)).await; - - match result { - Ok(Ok(cookie)) => Ok(cookie), - Ok(Err(e)) => Err(e.into()), - Err(e) => Err(e.into()), - } + Ok(tokio::task::spawn_blocking(move || client_clone.connect_for_cookie(entrypoint)).await??) } pub fn request_get_status() { @@ -143,6 +152,82 @@ pub fn request_get_status() { }); } +pub fn request_start_server(name: String, config_file: PathBuf) { + let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + runtime.block_on(async { + match crate::client::config::read_server_config_from_fs(&name, config_file).await { + Ok((stored_server, stored_configs)) => { + let (cookie, name, server, allow_insecure) = match stored_server { + StoredServer::Password(password_server) => { + let cookie = crate::client::state::obtain_cookie_from_password_server( + &password_server, + &stored_configs, + ) + .await + .ok() + .flatten(); + ( + cookie, + password_server.name, + password_server.server, + password_server.allow_insecure, + ) + } + StoredServer::Oidc(_) => { + todo!("OIDC server not implemented"); + } + }; + + if let Some(cookie) = cookie { + let mut unix_client = sock::UnixDomainClient::connect() + .await + .expect("Failed to connect to daemon"); + + unix_client + .send(JsonRequest::Start { + name, + server, + allow_insecure: allow_insecure.unwrap_or(false), + cookie, + }) + .await + .expect("Failed to send start command"); + + if let Ok(Some(response)) = unix_client.framed_reader.try_next().await { + match response { + JsonResponse::StartResult { + name, + success, + err_message, + } => { + if success { + println!("Started connection to server: {}", name); + } else { + eprintln!( + "Failed to start connection: {}", + err_message.unwrap_or("Unknown error".to_string()) + ); + std::process::exit(1); + } + } + _ => { + println!("Received unexpected response"); + } + } + } + } else { + eprintln!("Failed to obtain cookie, check logs for more information"); + std::process::exit(1); + } + } + Err(e) => { + eprintln!("Failed to get server: {}", e); + std::process::exit(1); + } + } + }); +} + pub fn request_stop_server() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); @@ -158,7 +243,7 @@ pub fn request_stop_server() { if let Ok(Some(response)) = client.framed_reader.try_next().await { match response { - JsonResponse::StopResult { server_name } => { + JsonResponse::StopResult { name: server_name } => { println!("Stopped connection to server: {}", server_name) } _ => { diff --git a/crates/openconnect-cli/src/main.rs b/crates/openconnect-cli/src/main.rs index c5c48f8..225e86d 100644 --- a/crates/openconnect-cli/src/main.rs +++ b/crates/openconnect-cli/src/main.rs @@ -6,15 +6,9 @@ mod sock; use clap::Parser; use cli::{Cli, Commands}; -use openconnect_core::{ - ip_info::IpInfo, - log::Logger, - storage::{StoredConfigs, StoredServer}, -}; +use openconnect_core::{ip_info::IpInfo, log::Logger, storage::StoredConfigs}; use std::{io::BufRead, path::PathBuf}; -use crate::sock::UnixDomainClient; - #[derive(serde::Serialize, serde::Deserialize)] pub enum JsonRequest { Start { @@ -30,10 +24,12 @@ pub enum JsonRequest { #[derive(serde::Serialize, serde::Deserialize)] pub enum JsonResponse { StartResult { - server_name: String, + name: String, + success: bool, + err_message: Option, }, StopResult { - server_name: String, + name: String, }, InfoResult { server_name: String, @@ -107,42 +103,7 @@ fn main() { match daemon::daemonize() { daemon::ForkResult::Parent => { - let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - runtime.block_on(async { - match crate::client::config::read_server_config_from_fs(&name, config_file) - .await - { - Ok((stored_server, stored_configs)) => { - let (cookie, name, server, allow_insecure) = match stored_server { - StoredServer::Password(password_server) => { - let cookie = crate::client::state::obtain_cookie_from_password_server( - &password_server, - &stored_configs, - ) - .await.unwrap(); - (cookie, password_server.name, password_server.server, password_server.allow_insecure) - } - StoredServer::Oidc(_) => { - todo!("OIDC server not implemented"); - } - }; - - if let Some(cookie) = cookie { - let mut unix_client = UnixDomainClient::connect().await.unwrap(); - let _ = unix_client.send(JsonRequest::Start { - name, - server, - allow_insecure: allow_insecure.unwrap_or(false), - cookie, - }).await; - } - } - Err(e) => { - eprintln!("Failed to get server: {}", e); - std::process::exit(1); - } - } - }); + crate::client::state::request_start_server(name, config_file); println!("The process will be running in the background, you should use cli to interact with it."); std::process::exit(0); } @@ -156,12 +117,6 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); - runtime.block_on(async { - crate::client::config::read_server_config_from_fs(&name, config_file) - .await - .expect("Failed to get server"); - }); - runtime.block_on(async { Logger::init().expect("Failed to initialize logger"); let _ = crate::server::start_daemon().await.inspect_err(|e| { diff --git a/crates/openconnect-cli/src/server/mod.rs b/crates/openconnect-cli/src/server/mod.rs index a33b358..e26a6a3 100644 --- a/crates/openconnect-cli/src/server/mod.rs +++ b/crates/openconnect-cli/src/server/mod.rs @@ -1,5 +1,5 @@ use crate::{ - client::state::get_vpnc_script, + client::state::{get_vpnc_script, StateError}, sock::{self, UnixDomainServer}, JsonRequest, JsonResponse, }; @@ -34,6 +34,39 @@ trait Acceptable { async fn try_accept(self); } +async fn connect_to_vpn_server( + name: &str, + server: &str, + allow_insecure: bool, + cookie: &str, +) -> Result, StateError> { + let vpncscript = get_vpnc_script()?; + + let config = ConfigBuilder::default() + .vpncscript(&vpncscript) + .loglevel(LogLevel::Info) + .build()?; + + let entrypoint = EntrypointBuilder::new() + .name(name) + .server(server) + .accept_insecure_cert(allow_insecure) + .cookie(cookie) + .enable_udp(true) + .build()?; + + let event_handler = EventHandlers::default(); + + let client = VpnClient::new(config, event_handler)?; + + let client_cloned = client.clone(); + tokio::task::spawn_blocking(move || { + let _ = client_cloned.connect(entrypoint); + }); + + Ok(client) +} + impl Acceptable for Arc { async fn try_accept(self) { if let Ok((stream, _)) = self.server.listener.accept().await { @@ -45,40 +78,38 @@ impl Acceptable for Arc { while let Ok(Some(command)) = framed_reader.try_next().await { match command { JsonRequest::Start { - name: server_name, - server: server_url, + name, + server, allow_insecure, cookie, } => { - let vpncscript = get_vpnc_script().unwrap(); - - let config = ConfigBuilder::default() - .vpncscript(&vpncscript) - .loglevel(LogLevel::Info) - .build() - .unwrap(); - - let entrypoint = EntrypointBuilder::new() - .name(&server_name) - .server(&server_url) - .accept_insecure_cert(allow_insecure) - .cookie(&cookie) - .enable_udp(true) - .build() - .unwrap(); - - let event_handler = EventHandlers::default(); - - let client = VpnClient::new(config, event_handler).unwrap(); - let client_cloned = client.clone(); - - tokio::task::spawn_blocking(move || { - let _ = client.connect(entrypoint); - }); - - { - let mut client_to_write = self.client.write().await; - *client_to_write = Some(client_cloned); + let connection_result = + connect_to_vpn_server(&name, &server, allow_insecure, &cookie) + .await; + + match connection_result { + Ok(client) => { + { + let mut client_to_write = self.client.write().await; + *client_to_write = Some(client); + } + let _ = framed_writer + .send(JsonResponse::StartResult { + name, + success: true, + err_message: None, + }) + .await; + } + Err(e) => { + let _ = framed_writer + .send(JsonResponse::StartResult { + name, + success: false, + err_message: Some(e.to_string()), + }) + .await; + } } } @@ -92,7 +123,7 @@ impl Acceptable for Arc { // ignore send error let _ = framed_writer - .send(JsonResponse::StopResult { server_name }) + .send(JsonResponse::StopResult { name: server_name }) .await; unsafe { libc::raise(libc::SIGTERM); diff --git a/crates/openconnect-gui/src-tauri/src/state.rs b/crates/openconnect-gui/src-tauri/src/state.rs index e228073..f28fe9b 100644 --- a/crates/openconnect-gui/src-tauri/src/state.rs +++ b/crates/openconnect-gui/src-tauri/src/state.rs @@ -40,10 +40,7 @@ pub enum StateError { #[derive(Debug, Clone)] pub enum VpnEvent { - Status { - status: StatusPayload, - server_name: Option, - }, + Status { status: StatusPayload }, } #[derive(serde::Serialize, Debug, Clone)] @@ -92,7 +89,7 @@ impl AppState { let handle = handle.clone(); let app_system_tray: State<'_, Arc> = handle.state(); match event { - VpnEvent::Status { status, .. } => { + VpnEvent::Status { status } => { let result = handle.emit_all("vpnStatus", Some(status)); app_system_tray.recreate(&handle).await.unwrap(); if let Err(e) = result { @@ -120,14 +117,8 @@ impl AppState { } pub async fn trigger_state_retrieve(&self) -> Result<(), StateError> { - let (status, server_name) = self.get_status_and_name().await?; - Ok(self - .event_tx - .send(VpnEvent::Status { - status, - server_name, - }) - .await?) + let (status, _server_name) = self.get_status_and_name().await?; + Ok(self.event_tx.send(VpnEvent::Status { status }).await?) } pub async fn connect_with_server_name(&self, server_name: &str) -> Result<(), StateError> { @@ -161,7 +152,7 @@ impl AppState { .enable_udp(true) .build()?; - let event_handlers = self.create_event_handler(server_name); + let event_handlers = self.create_event_handler(); let client = VpnClient::new(config, event_handlers)?; { @@ -220,7 +211,7 @@ impl AppState { .accept_insecure_cert(oidc_server.allow_insecure.unwrap_or(false)) .build()?; - let event_handlers = self.create_event_handler(server_name); + let event_handlers = self.create_event_handler(); let client = VpnClient::new(config, event_handlers)?; { @@ -245,24 +236,17 @@ impl AppState { Ok(()) } - pub fn create_event_handler( - &self, - server_name: &str, - ) -> openconnect_core::events::EventHandlers { + pub fn create_event_handler(&self) -> openconnect_core::events::EventHandlers { let event_tx_for_state = self.event_tx.clone(); let event_tx_for_cert = self.event_tx.clone(); - let server_name_for_state = server_name.to_string(); - let server_name_for_cert = server_name.to_string(); EventHandlers::default() .with_handle_connection_state_change(move |state| { let event_tx = event_tx_for_state.clone(); - let server_name = Some(server_name_for_state.to_string()); tauri::async_runtime::spawn(async move { let _ = event_tx .send(VpnEvent::Status { status: state.into(), - server_name, }) .await; // ignore the result @@ -270,7 +254,6 @@ impl AppState { }) .with_handle_peer_cert_invalid(move |reason| { let event_tx = event_tx_for_cert.clone(); - let server_name = Some(server_name_for_cert.to_string()); let reason = reason.to_string(); tauri::async_runtime::spawn(async move { let _ = event_tx @@ -279,7 +262,6 @@ impl AppState { status: "ERROR".to_string(), message: Some(format!("Peer certificate invalid, if you want to connect this insecure server, please tick 'Allow insecure' in the config. Server fingerprint: {}", reason)), }, - server_name, }) .await; // ignore the result From 128d1a0ab8d9f7beaece889d64a48a2a9859a97d Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Wed, 17 Apr 2024 14:27:09 +0800 Subject: [PATCH 13/14] feat: impl oidc device auth for cli --- Cargo.lock | 3 +- README.md | 40 +++++++++++---- crates/openconnect-cli/Cargo.toml | 1 + crates/openconnect-cli/src/client/state.rs | 57 +++++++++++++++++++-- crates/openconnect-gui/src-tauri/Cargo.toml | 4 +- crates/openconnect-oidc/Cargo.toml | 2 +- crates/openconnect-oidc/src/oidc_device.rs | 10 +--- 7 files changed, 91 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dfe4fe..71f2e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2872,6 +2872,7 @@ dependencies = [ "home", "libc", "openconnect-core", + "openconnect-oidc", "serde", "serde_json", "sudo", @@ -2941,7 +2942,7 @@ dependencies = [ [[package]] name = "openconnect-oidc" -version = "0.1.0" +version = "0.1.1" dependencies = [ "openidconnect", "reqwest 0.12.3", diff --git a/README.md b/README.md index 2620e09..6dbd32e 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,17 @@ This is a cross-platform GUI client for OpenConnect, written in Rust and designe Usage: openconnect Commands: - start Connect to a VPN server and run in daemon mode [aliases: connect, run] - status Get the current VPN connection status [aliases: info, stat] - stop Close the current connection and exit the daemon process [aliases: kill, disconnect] - add Add new VPN server configuration to local config file [aliases: new, create, insert] - delete Delete a VPN server configuration from local config file [aliases: rm, remove, del] - list List all VPN server configurations in local config file [aliases: ls, l] - logs Show logs of the daemon process [aliases: log] - help Print this message or the help of the given subcommand(s) + start Connect to a VPN server and run in daemon mode [aliases: connect, run] + status Get the current VPN connection status [aliases: info, stat] + stop Close the current connection and exit the daemon process [aliases: kill, disconnect] + add Add new VPN server configuration to local config file [aliases: new, create, insert] + import Import VPN server configurations from a base64 encoded string + export Export VPN server configurations to a base64 encoded string + delete Delete a VPN server configuration from local config file [aliases: rm, remove, del] + list List all VPN server configurations in local config file [aliases: ls, l] + logs Show logs of the daemon process [aliases: log] + gen-complete Generate shell completion script + help Print this message or the help of the given subcommand(s) Options: -h, --help Print help @@ -90,6 +93,24 @@ This is a cross-platform GUI client for OpenConnect, written in Rust and designe -h, --help Print help ``` +### Generate shell completion script + +- ZSH (Oh My Zsh!) + + ```bash + mkdir -p ~/.oh-my-zsh/custom/plugins/openconnect + openconnect gen-complete zsh > ~/.oh-my-zsh/custom/plugins/openconnect/openconnect.plugin.zsh + echo "plugins+=(openconnect)" >> ~/.zshrc + ``` + +- Bash + + ```bash + mkdir -p ~/.bash_completion + openconnect gen-complete bash > ~/.bash_completion/openconnect + echo "source ~/.bash_completion/openconnect" >> ~/.bashrc + ``` + ## Build - Read the [System Requirements](./crates/openconnect-sys/README.md) for environment setup @@ -139,8 +160,9 @@ Special thanks to (MORE THAN) the following projects and technologies for making - [x] implement oidc login - [x] implement logs - [x] tracing file rotation + - [ ] optimize log search - [x] implement CLI - [x] Add/Remove configurations - [x] Daemon mode - [x] Password login - - [ ] OIDC login + - [x] OIDC login diff --git a/crates/openconnect-cli/Cargo.toml b/crates/openconnect-cli/Cargo.toml index 56cdaae..1857c04 100644 --- a/crates/openconnect-cli/Cargo.toml +++ b/crates/openconnect-cli/Cargo.toml @@ -16,6 +16,7 @@ futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } libc = { workspace = true } +openconnect-oidc = { path = "../openconnect-oidc", version = "0.1.1" } openconnect-core = { path = "../openconnect-core", version = "0.1.1" } sudo = { workspace = true } tracing = { workspace = true } diff --git a/crates/openconnect-cli/src/client/state.rs b/crates/openconnect-cli/src/client/state.rs index 8ef0a82..da2e506 100644 --- a/crates/openconnect-cli/src/client/state.rs +++ b/crates/openconnect-cli/src/client/state.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use crate::{sock, JsonRequest, JsonResponse}; use comfy_table::Table; use futures::TryStreamExt; @@ -7,9 +5,14 @@ use openconnect_core::{ config::{ConfigBuilder, EntrypointBuilder, LogLevel}, events::EventHandlers, result::OpenconnectError, - storage::{PasswordServer, StoredConfigs, StoredServer}, + storage::{OidcServer, PasswordServer, StoredConfigs, StoredServer}, Connectable, VpnClient, }; +use openconnect_oidc::{ + obtain_cookie_by_oidc_token, + oidc_device::{OpenIDDeviceAuth, OpenIDDeviceAuthConfig, OpenIDDeviceAuthError}, +}; +use std::path::PathBuf; #[allow(clippy::enum_variant_names)] #[derive(thiserror::Error, Debug)] @@ -22,6 +25,9 @@ pub enum StateError { #[error("Tokio task error: {0}")] TokioTaskError(#[from] tokio::task::JoinError), + + #[error("OpenID device auth error: {0}")] + OpenIDAuthError(#[from] OpenIDDeviceAuthError), } pub fn get_vpnc_script() -> Result { @@ -68,6 +74,33 @@ pub async fn obtain_cookie_from_password_server( Ok(tokio::task::spawn_blocking(move || client_clone.connect_for_cookie(entrypoint)).await??) } +pub async fn obtain_cookie_from_oidc_server( + oidc_server: &OidcServer, + _stored_configs: &StoredConfigs, +) -> Result, StateError> { + let openid_config = OpenIDDeviceAuthConfig { + issuer_url: oidc_server.issuer.clone(), + client_id: oidc_server.client_id.clone(), + client_secret: oidc_server.client_secret.clone(), + }; + + let mut openid = OpenIDDeviceAuth::new(openid_config).await?; + let device_auth_response = openid.exchange_device_token().await?; + let verification_url = device_auth_response.verification_uri().url(); + let user_code = device_auth_response.user_code(); + println!( + "Please visit {} and enter code {}", + verification_url, + user_code.secret() + ); + + let token = openid + .exchange_token(&device_auth_response, tokio::time::sleep, None) + .await?; + + Ok(obtain_cookie_by_oidc_token(&oidc_server.server, &token).await) +} + pub fn request_get_status() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create runtime"); @@ -166,6 +199,7 @@ pub fn request_start_server(name: String, config_file: PathBuf) { .await .ok() .flatten(); + ( cookie, password_server.name, @@ -173,8 +207,21 @@ pub fn request_start_server(name: String, config_file: PathBuf) { password_server.allow_insecure, ) } - StoredServer::Oidc(_) => { - todo!("OIDC server not implemented"); + StoredServer::Oidc(oidc_server) => { + let cookie = crate::client::state::obtain_cookie_from_oidc_server( + &oidc_server, + &stored_configs, + ) + .await + .ok() + .flatten(); + + ( + cookie, + oidc_server.name, + oidc_server.server, + oidc_server.allow_insecure, + ) } }; diff --git a/crates/openconnect-gui/src-tauri/Cargo.toml b/crates/openconnect-gui/src-tauri/Cargo.toml index 4582499..f74ca35 100644 --- a/crates/openconnect-gui/src-tauri/Cargo.toml +++ b/crates/openconnect-gui/src-tauri/Cargo.toml @@ -20,8 +20,8 @@ tauri = { version = "1.6.1", features = [ "os-all", "global-shortcut-all", "app- dotenvy = { workspace = true } lazy_static = { workspace = true } open = "5.1.2" -openconnect-core = { path = "../../openconnect-core" } -openconnect-oidc = { path = "../../openconnect-oidc" } +openconnect-core = { path = "../../openconnect-core", version = "0.1.1" } +openconnect-oidc = { path = "../../openconnect-oidc", version = "0.1.1" } openidconnect = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/openconnect-oidc/Cargo.toml b/crates/openconnect-oidc/Cargo.toml index 10480f0..738d4f5 100644 --- a/crates/openconnect-oidc/Cargo.toml +++ b/crates/openconnect-oidc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openconnect-oidc" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] diff --git a/crates/openconnect-oidc/src/oidc_device.rs b/crates/openconnect-oidc/src/oidc_device.rs index 4868c91..6a06636 100644 --- a/crates/openconnect-oidc/src/oidc_device.rs +++ b/crates/openconnect-oidc/src/oidc_device.rs @@ -7,7 +7,7 @@ use openidconnect::{ }, reqwest::async_http_client, AdditionalProviderMetadata, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, - IssuerUrl, ProviderMetadata, RedirectUrl, TokenResponse, + IssuerUrl, ProviderMetadata, TokenResponse, }; use std::{future::Future, time::Duration}; @@ -42,7 +42,6 @@ type DeviceProviderMetadata = ProviderMetadata< pub struct OpenIDDeviceAuthConfig { pub issuer_url: String, - pub redirect_uri: Option, pub client_id: String, pub client_secret: Option, } @@ -77,19 +76,14 @@ impl OpenIDDeviceAuth { .device_authorization_endpoint .clone(); - let redirect_uri = config.redirect_uri.and_then(|x| RedirectUrl::new(x).ok()); let client_id = ClientId::new(config.client_id); let client_secret = config.client_secret.map(ClientSecret::new); - let mut client = + let client = CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret) .set_device_authorization_uri(device_authorization_endpoint) .set_auth_type(AuthType::RequestBody); - if let Some(redirect_uri) = redirect_uri { - client = client.set_redirect_uri(redirect_uri); - } - Ok(OpenIDDeviceAuth { client }) } From ed27ec90bbbb5f855a5db3884fd4b773324d2948 Mon Sep 17 00:00:00 2001 From: hlhr202 Date: Wed, 17 Apr 2024 14:28:05 +0800 Subject: [PATCH 14/14] chore: clean --- Cargo.lock | 7 --- Cargo.toml | 4 +- crates/openconnect-build/Cargo.toml | 10 ---- crates/openconnect-build/src/lib.rs | 71 ---------------------------- crates/openconnect-build/src/main.rs | 3 -- 5 files changed, 2 insertions(+), 93 deletions(-) delete mode 100644 crates/openconnect-build/Cargo.toml delete mode 100644 crates/openconnect-build/src/lib.rs delete mode 100644 crates/openconnect-build/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 71f2e3a..1da4ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2850,13 +2850,6 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openconnect-build" -version = "0.1.0" -dependencies = [ - "pkg-config", -] - [[package]] name = "openconnect-cli" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 3b058cf..afe1984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [workspace] resolver = "2" members = [ - "crates/openconnect-build", "crates/openconnect-sys", "crates/openconnect-core", "crates/openconnect-gui/src-tauri", - "crates/openconnect-cli", "crates/openconnect-oidc", + "crates/openconnect-cli", + "crates/openconnect-oidc", ] [workspace.dependencies] diff --git a/crates/openconnect-build/Cargo.toml b/crates/openconnect-build/Cargo.toml deleted file mode 100644 index 9522ac5..0000000 --- a/crates/openconnect-build/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "openconnect-build" -version = "0.1.0" -edition = "2021" - -[dependencies] -pkg-config = "0.3.30" - -[lib] -crate-type = ["lib"] \ No newline at end of file diff --git a/crates/openconnect-build/src/lib.rs b/crates/openconnect-build/src/lib.rs deleted file mode 100644 index 820ea8e..0000000 --- a/crates/openconnect-build/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -#[macro_export] -macro_rules! print_build_warning { - ($($arg:tt)*) => { - println!("cargo:warning={}", format_args!($($arg)*)); - }; -} - -pub fn is_msys64_shell() -> bool { - std::env::var("MSYSTEM").is_ok() -} - -pub fn get_cygpath(path: &str) -> String { - use std::ffi::OsString; - - let cygpath = if is_msys64_shell() { - OsString::from("cygpath") - } else { - let system_drive = std::env::var("SystemDrive").expect("SystemDrive not found"); - let cygpath = format!("{}\\msys64\\usr\\bin\\cygpath", system_drive); - OsString::from(&cygpath) - }; - - let cygpath_cmd = std::process::Command::new(cygpath) - .arg("-w") - .arg(path) - .output() - .expect("failed to execute cygpath"); - - String::from_utf8(cygpath_cmd.stdout).expect("cygpath output is not utf8") -} - -pub fn resolve_mingw64_lib_path() { - let lib_path = get_cygpath("/mingw64/lib"); - print_build_warning!("mingw64_lib_path: {}", lib_path); - println!("cargo:rustc-link-search={}", lib_path); -} - -pub fn try_pkg_config(libs: Vec<&str>) { - #[cfg(target_os = "windows")] - { - std::env::set_var("PKG_CONFIG", "pkg-config"); - std::env::set_var( - "PKG_CONFIG_PATH", - "/mingw64/lib/pkgconfig:/mingw64/share/pkgconfig", - ); - std::env::set_var("PKG_CONFIG_SYSTEM_INCLUDE_PATH", "/mingw64/include"); - std::env::set_var("PKG_CONFIG_SYSTEM_LIBRARY_PATH", "/mingw64/lib"); - } - - #[cfg(target_os = "macos")] - { - std::env::set_var( - "PKG_CONFIG_PATH", - "/usr/lib/pkgconfig:/usr/local/lib/pkgconfig:/opt/local/lib/pkgconfig:/opt/homebrew/lib/pkgconfig", - ); - } - - let mut conf = pkg_config::Config::new(); - - for lib in libs { - let result = conf.statik(true).probe(lib); - if result.is_err() { - print_build_warning!("{} not found", lib); - } - } -} - -#[test] -fn test_prob() { - try_pkg_config(vec!["openssl", "libxml-2.0", "zlib", "liblz4"]) -} diff --git a/crates/openconnect-build/src/main.rs b/crates/openconnect-build/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/crates/openconnect-build/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -}