diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..419fc3d --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,62 @@ +FROM ubuntu:18.04 + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=1.75.0 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + sudo \ + ca-certificates \ + curl \ + gnupg \ + git \ + less \ + software-properties-common \ + # Tauri dependencies + libwebkit2gtk-4.0-dev build-essential wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev; \ + # Install openconnect + add-apt-repository ppa:yuezk/globalprotect-openconnect; \ + apt-get update; \ + apt-get install -y openconnect libopenconnect-dev; \ + # Create a non-root user + groupadd --gid $USER_GID $USERNAME; \ + useradd --uid $USER_UID --gid $USER_GID -m $USERNAME; \ + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME; \ + chmod 0440 /etc/sudoers.d/$USERNAME; \ + # Install Node.js + mkdir -p /etc/apt/keyrings; \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \ + apt-get update; \ + apt-get install -y nodejs; \ + corepack enable; \ + # Install diff-so-fancy + npm install -g diff-so-fancy; \ + # Install Rust + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $RUST_VERSION; \ + chown -R $USERNAME:$USERNAME $RUSTUP_HOME $CARGO_HOME; \ + rustup --version; \ + cargo --version; \ + rustc --version + +USER $USERNAME + +# Install Oh My Zsh +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.5/zsh-in-docker.sh)" -- \ + -t https://github.com/denysdovhan/spaceship-prompt \ + -a 'SPACESHIP_PROMPT_ADD_NEWLINE="false"' \ + -a 'SPACESHIP_PROMPT_SEPARATE_LINE="false"' \ + -p git \ + -p https://github.com/zsh-users/zsh-autosuggestions \ + -p https://github.com/zsh-users/zsh-completions; \ + # Change the default shell + sudo chsh -s /bin/zsh $USERNAME; \ + # Change the XTERM to xterm-256color + sed -i 's/TERM=xterm/TERM=xterm-256color/g' $HOME/.zshrc; diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c48cded --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "runArgs": [ + "--privileged", + "--cap-add=NET_ADMIN", + "--device=/dev/net/tun" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20282f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +/target +.pnpm-store +.env diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..268bdcb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "streetsidesoftware.code-spell-checker", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6426a3a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,51 @@ +{ + "cSpell.words": [ + "authcookie", + "bincode", + "chacha", + "clientos", + "datetime", + "disconnectable", + "distro", + "dotenv", + "dotenvy", + "getconfig", + "gpapi", + "gpauth", + "gpclient", + "gpcommon", + "gpgui", + "gpservice", + "hidpi", + "jnlp", + "LOGNAME", + "oneshot", + "openconnect", + "pkexec", + "Prelogin", + "prelogon", + "prelogonuserauthcookie", + "repr", + "reqwest", + "roxmltree", + "rspc", + "servercert", + "specta", + "sysinfo", + "tanstack", + "tauri", + "tempfile", + "thiserror", + "tungstenite", + "unistd", + "unlisten", + "urlencoding", + "userauthcookie", + "utsbuf", + "Vite", + "vpnc", + "vpninfo", + "wmctrl", + "XAUTHORITY" + ] +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f93fe55 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5052 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.5", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa 1.0.10", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compile-time" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55ede5279d4d7c528906853743abeb26353ae1e6c440fcd6d18316c2c2dd903" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "rustc_version", + "semver", + "time", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "ctor" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "embed-resource" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.8", + "vswhom", + "winreg 0.51.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.0", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gpapi" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "base64 0.21.5", + "chacha20poly1305", + "dotenvy_macro", + "log", + "redact-engine", + "regex", + "reqwest", + "roxmltree", + "serde", + "serde_json", + "specta", + "specta-macros", + "tauri", + "tempfile", + "thiserror", + "tokio", + "url", + "urlencoding", + "users", + "whoami", +] + +[[package]] +name = "gpauth" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "clap", + "compile-time", + "env_logger", + "gpapi", + "log", + "regex", + "serde_json", + "tauri", + "tauri-build", + "tempfile", + "tokio", + "tokio-util", + "webkit2gtk", +] + +[[package]] +name = "gpclient" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "clap", + "compile-time", + "directories", + "env_logger", + "gpapi", + "inquire", + "log", + "openconnect", + "reqwest", + "serde_json", + "sysinfo", + "tempfile", + "tokio", + "whoami", +] + +[[package]] +name = "gpservice" +version = "2.0.0-beta.1" +dependencies = [ + "anyhow", + "axum", + "clap", + "compile-time", + "env_logger", + "futures", + "gpapi", + "log", + "openconnect", + "serde_json", + "tokio", + "tokio-util", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.0", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.3", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inquire" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +dependencies = [ + "bitflags 1.3.2", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openconnect" +version = "2.0.0-beta.1" +dependencies = [ + "cc", + "is_executable", + "log", +] + +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.1+3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "plist" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +dependencies = [ + "base64 0.21.5", + "indexmap 2.1.0", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.11", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redact-engine" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4ba41ad2b45cb0d6df9719881b43fca399dfa084debba338a08d5d599b52c" +dependencies = [ + "anyhow", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_regex", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.11", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "roxmltree" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa 1.0.10", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "specta" +version = "2.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899e1de0122914c5573ca81ced566c5aaeebf2c363595ee3dfdaac58f33ea351" +dependencies = [ + "document-features", + "indoc", + "once_cell", + "paste", + "serde", + "serde_json", + "specta-macros", + "thiserror", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0de71d47f9076754480433ca4fdcbde756312b56e7991ac17f2cb2435897ed" +dependencies = [ + "Inflector", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", + "termcolor", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr 0.15.6", + "heck 0.4.1", + "pkg-config", + "toml 0.8.8", + "version-compare 0.1.1", +] + +[[package]] +name = "tao" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f5aefd6be4cd3ad3f047442242fd9f57cbfb3e565379f66b5e14749364fa4f" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "tauri" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" +dependencies = [ + "anyhow", + "bytes", + "cocoa", + "dirs-next", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http 0.2.11", + "ignore", + "objc", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "reqwest", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-build" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" +dependencies = [ + "base64 0.21.5", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" +dependencies = [ + "gtk", + "http 0.2.11", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" +dependencies = [ + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa 1.0.10", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.0.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.11", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http 0.2.11", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..13ca1e3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[workspace] +resolver = "2" + +members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth"] + +[workspace.package] +version = "2.0.0-beta.1" +authors = ["Kevin Yue "] +homepage = "https://github.com/yuezk/GlobalProtect-openconnect" +edition = "2021" +license = "GPL-3.0" + +[workspace.dependencies] +anyhow = "1.0" +base64 = "0.21" +clap = { version = "4.4.2", features = ["derive"] } +ctrlc = "3.4" +directories = "5.0" +env_logger = "0.10" +is_executable = "1.0" +log = "0.4" +regex = "1" +reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] } +roxmltree = "0.18" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sysinfo = "0.29" +tempfile = "3.8" +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +url = "2.4" +urlencoding = "2.1.3" +axum = "0.7" +futures = "0.3" +futures-util = "0.3" +tokio-tungstenite = "0.20.1" +specta = "=2.0.0-rc.1" +specta-macros = "=2.0.0-rc.1" +users = "0.11" +whoami = "1" +tauri = { version = "1.5" } +thiserror = "1" +redact-engine = "0.1" +dotenvy_macro = "0.15" +compile-time = "0.2" + +[profile.release] +opt-level = 'z' # Optimize for size +lto = true # Enable link-time optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* diff --git a/apps/gpauth/Cargo.toml b/apps/gpauth/Cargo.toml new file mode 100644 index 0000000..f429259 --- /dev/null +++ b/apps/gpauth/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gpauth" +version.workspace = true +edition.workspace = true +license.workspace = true + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[dependencies] +gpapi = { path = "../../crates/gpapi", features = ["tauri"] } +anyhow.workspace = true +clap.workspace = true +env_logger.workspace = true +log.workspace = true +regex.workspace = true +serde_json.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tempfile.workspace = true +webkit2gtk = "0.18.2" +tauri = { workspace = true, features = ["http-all"] } +compile-time.workspace = true diff --git a/apps/gpauth/build.rs b/apps/gpauth/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/apps/gpauth/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/gpauth/icons/128x128.png b/apps/gpauth/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/apps/gpauth/icons/128x128.png differ diff --git a/apps/gpauth/icons/128x128@2x.png b/apps/gpauth/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/apps/gpauth/icons/128x128@2x.png differ diff --git a/apps/gpauth/icons/32x32.png b/apps/gpauth/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/apps/gpauth/icons/32x32.png differ diff --git a/apps/gpauth/icons/icon.icns b/apps/gpauth/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/apps/gpauth/icons/icon.icns differ diff --git a/apps/gpauth/icons/icon.ico b/apps/gpauth/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/apps/gpauth/icons/icon.ico differ diff --git a/apps/gpauth/icons/icon.png b/apps/gpauth/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/apps/gpauth/icons/icon.png differ diff --git a/apps/gpauth/index.html b/apps/gpauth/index.html new file mode 100644 index 0000000..e5aa110 --- /dev/null +++ b/apps/gpauth/index.html @@ -0,0 +1,11 @@ + + + + + + GlobalProtect Login + + +

Redirecting to GlobalProtect Login...

+ + diff --git a/apps/gpauth/src/auth_window.rs b/apps/gpauth/src/auth_window.rs new file mode 100644 index 0000000..11f3ae3 --- /dev/null +++ b/apps/gpauth/src/auth_window.rs @@ -0,0 +1,449 @@ +use std::{ + rc::Rc, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::bail; +use gpapi::{ + auth::SamlAuthData, + portal::{prelogin, Prelogin}, + utils::{redact::redact_uri, window::WindowExt}, +}; +use log::{info, warn}; +use regex::Regex; +use tauri::{AppHandle, Window, WindowEvent, WindowUrl}; +use tokio::sync::{mpsc, oneshot, RwLock}; +use tokio_util::sync::CancellationToken; +use webkit2gtk::{ + gio::Cancellable, + glib::{GString, TimeSpan}, + LoadEvent, URIResponse, URIResponseExt, WebContextExt, WebResource, WebResourceExt, WebView, + WebViewExt, WebsiteDataManagerExtManual, WebsiteDataTypes, +}; + +enum AuthDataError { + /// 1. Found auth data in headers/body but it's invalid + /// 2. Loaded an empty page, failed to load page. etc. + Invalid, + /// No auth data found in headers/body + NotFound, +} + +type AuthResult = Result; + +pub(crate) struct AuthWindow<'a> { + app_handle: AppHandle, + server: &'a str, + saml_request: &'a str, + user_agent: &'a str, + clean: bool, +} + +impl<'a> AuthWindow<'a> { + pub fn new(app_handle: AppHandle) -> Self { + Self { + app_handle, + server: "", + saml_request: "", + user_agent: "", + clean: false, + } + } + + pub fn server(mut self, server: &'a str) -> Self { + self.server = server; + self + } + + pub fn saml_request(mut self, saml_request: &'a str) -> Self { + self.saml_request = saml_request; + self + } + + pub fn user_agent(mut self, user_agent: &'a str) -> Self { + self.user_agent = user_agent; + self + } + + pub fn clean(mut self, clean: bool) -> Self { + self.clean = clean; + self + } + + pub async fn open(&self) -> anyhow::Result { + info!("Open auth window, user_agent: {}", self.user_agent); + + let window = Window::builder(&self.app_handle, "auth_window", WindowUrl::default()) + .title("GlobalProtect Login") + .user_agent(self.user_agent) + .focused(true) + .visible(false) + .center() + .build()?; + + let window = Arc::new(window); + + let cancel_token = CancellationToken::new(); + let cancel_token_clone = cancel_token.clone(); + + window.on_window_event(move |event| { + if let WindowEvent::CloseRequested { .. } = event { + cancel_token_clone.cancel(); + } + }); + + let window_clone = Arc::clone(&window); + let timeout_secs = 15; + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(timeout_secs)).await; + let visible = window_clone.is_visible().unwrap_or(false); + if !visible { + info!("Try to raise auth window after {} seconds", timeout_secs); + raise_window(&window_clone); + } + }); + + tokio::select! { + _ = cancel_token.cancelled() => { + bail!("Auth cancelled"); + } + saml_result = self.auth_loop(&window) => { + window.close()?; + saml_result + } + } + } + + async fn auth_loop(&self, window: &Arc) -> anyhow::Result { + let saml_request = self.saml_request.to_string(); + let (auth_result_tx, mut auth_result_rx) = mpsc::unbounded_channel::(); + let raise_window_cancel_token: Arc>> = Default::default(); + + if self.clean { + clear_webview_cookies(window).await?; + } + + let raise_window_cancel_token_clone = Arc::clone(&raise_window_cancel_token); + window.with_webview(move |wv| { + let wv = wv.inner(); + + // Load the initial SAML request + load_saml_request(&wv, &saml_request); + + let auth_result_tx_clone = auth_result_tx.clone(); + wv.connect_load_changed(move |wv, event| { + if event == LoadEvent::Started { + let Ok(mut cancel_token) = raise_window_cancel_token_clone.try_write() else { + return; + }; + + // Cancel the raise window task + if let Some(cancel_token) = cancel_token.take() { + cancel_token.cancel(); + } + return; + } + + if event != LoadEvent::Finished { + return; + } + + if let Some(main_resource) = wv.main_resource() { + let uri = main_resource.uri().unwrap_or("".into()); + + if uri.is_empty() { + warn!("Loaded an empty uri"); + send_auth_result(&auth_result_tx_clone, Err(AuthDataError::Invalid)); + return; + } + + info!("Loaded uri: {}", redact_uri(&uri)); + read_auth_data(&main_resource, auth_result_tx_clone.clone()); + } + }); + + wv.connect_load_failed_with_tls_errors(|_wv, uri, cert, err| { + let redacted_uri = redact_uri(uri); + warn!( + "Failed to load uri: {} with error: {}, cert: {}", + redacted_uri, err, cert + ); + true + }); + + wv.connect_load_failed(move |_wv, _event, uri, err| { + let redacted_uri = redact_uri(uri); + warn!("Failed to load uri: {} with error: {}", redacted_uri, err); + send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); + // true to stop other handlers from being invoked for the event. false to propagate the event further. + true + }); + })?; + + let portal = self.server.to_string(); + let user_agent = self.user_agent.to_string(); + + loop { + if let Some(auth_result) = auth_result_rx.recv().await { + match auth_result { + Ok(auth_data) => return Ok(auth_data), + Err(AuthDataError::NotFound) => { + info!("No auth data found, it may not be the /SAML20/SP/ACS endpoint"); + + // The user may need to interact with the auth window, raise it in 3 seconds + if !window.is_visible().unwrap_or(false) { + let window = Arc::clone(window); + let cancel_token = CancellationToken::new(); + + raise_window_cancel_token + .write() + .await + .replace(cancel_token.clone()); + + tokio::spawn(async move { + let delay_secs = 1; + + info!("Raise window in {} second(s)", delay_secs); + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(delay_secs)) => { + raise_window(&window); + } + _ = cancel_token.cancelled() => { + info!("Raise window cancelled"); + } + } + }); + } + } + Err(AuthDataError::Invalid) => { + info!("Got invalid auth data, retrying..."); + + window.with_webview(|wv| { + let wv = wv.inner(); + wv.run_javascript(r#" + var loading = document.createElement("div"); + loading.innerHTML = '
Got invalid token, retrying...
'; + loading.style = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.85); z-index: 99999;"; + document.body.appendChild(loading); + "#, + Cancellable::NONE, + |_| info!("Injected loading element successfully"), + ); + })?; + + let saml_request = portal_prelogin(&portal, &user_agent).await?; + window.with_webview(move |wv| { + let wv = wv.inner(); + load_saml_request(&wv, &saml_request); + })?; + } + } + } + } + } +} + +fn raise_window(window: &Arc) { + let visible = window.is_visible().unwrap_or(false); + if !visible { + if let Err(err) = window.raise() { + warn!("Failed to raise window: {}", err); + } + } +} + +pub(crate) async fn portal_prelogin(portal: &str, user_agent: &str) -> anyhow::Result { + info!("Portal prelogin..."); + match prelogin(portal, user_agent).await? { + Prelogin::Saml(prelogin) => Ok(prelogin.saml_request().to_string()), + Prelogin::Standard(_) => Err(anyhow::anyhow!("Received non-SAML prelogin response")), + } +} + +fn send_auth_result(auth_result_tx: &mpsc::UnboundedSender, auth_result: AuthResult) { + if let Err(err) = auth_result_tx.send(auth_result) { + warn!("Failed to send auth event: {}", err); + } +} + +fn load_saml_request(wv: &Rc, saml_request: &str) { + if saml_request.starts_with("http") { + info!("Load the SAML request as URI..."); + wv.load_uri(saml_request); + } else { + info!("Load the SAML request as HTML..."); + wv.load_html(saml_request, None); + } +} + +fn read_auth_data_from_headers(response: &URIResponse) -> AuthResult { + response.http_headers().map_or_else( + || { + info!("No headers found in response"); + Err(AuthDataError::NotFound) + }, + |mut headers| match headers.get("saml-auth-status") { + Some(status) if status == "1" => { + let username = headers.get("saml-username").map(GString::into); + let prelogin_cookie = headers.get("prelogin-cookie").map(GString::into); + let portal_userauthcookie = headers.get("portal-userauthcookie").map(GString::into); + + if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { + return Ok(SamlAuthData::new( + username.unwrap(), + prelogin_cookie, + portal_userauthcookie, + )); + } + + info!("Found invalid auth data in headers"); + Err(AuthDataError::Invalid) + } + Some(status) => { + info!("Found invalid SAML status: {} in headers", status); + Err(AuthDataError::Invalid) + } + None => { + info!("No saml-auth-status header found"); + Err(AuthDataError::NotFound) + } + }, + ) +} + +fn read_auth_data_from_body(main_resource: &WebResource, callback: F) +where + F: FnOnce(AuthResult) + Send + 'static, +{ + main_resource.data(Cancellable::NONE, |data| match data { + Ok(data) => { + let html = String::from_utf8_lossy(&data); + callback(read_auth_data_from_html(&html)); + } + Err(err) => { + info!("Failed to read response body: {}", err); + callback(Err(AuthDataError::Invalid)) + } + }); +} + +fn read_auth_data_from_html(html: &str) -> AuthResult { + if html.contains("Temporarily Unavailable") { + info!("Found 'Temporarily Unavailable' in HTML, auth failed"); + return Err(AuthDataError::Invalid); + } + + match parse_xml_tag(html, "saml-auth-status") { + Some(saml_status) if saml_status == "1" => { + let username = parse_xml_tag(html, "saml-username"); + let prelogin_cookie = parse_xml_tag(html, "prelogin-cookie"); + let portal_userauthcookie = parse_xml_tag(html, "portal-userauthcookie"); + + if SamlAuthData::check(&username, &prelogin_cookie, &portal_userauthcookie) { + return Ok(SamlAuthData::new( + username.unwrap(), + prelogin_cookie, + portal_userauthcookie, + )); + } + + info!("Found invalid auth data in HTML"); + Err(AuthDataError::Invalid) + } + Some(status) => { + info!("Found invalid SAML status {} in HTML", status); + Err(AuthDataError::Invalid) + } + None => { + info!("No auth data found in HTML"); + Err(AuthDataError::NotFound) + } + } +} + +fn read_auth_data(main_resource: &WebResource, auth_result_tx: mpsc::UnboundedSender) { + if main_resource.response().is_none() { + info!("No response found in main resource"); + send_auth_result(&auth_result_tx, Err(AuthDataError::Invalid)); + return; + } + + let response = main_resource.response().unwrap(); + info!("Trying to read auth data from response headers..."); + + match read_auth_data_from_headers(&response) { + Ok(auth_data) => { + info!("Got auth data from headers"); + send_auth_result(&auth_result_tx, Ok(auth_data)); + } + Err(AuthDataError::Invalid) => { + info!("Found invalid auth data in headers, trying to read from body..."); + read_auth_data_from_body(main_resource, move |auth_result| { + // Since we have already found invalid auth data in headers, which means this could be the `/SAML20/SP/ACS` endpoint + // any error result from body should be considered as invalid, and trigger a retry + let auth_result = auth_result.map_err(|_| AuthDataError::Invalid); + send_auth_result(&auth_result_tx, auth_result); + }); + } + Err(AuthDataError::NotFound) => { + info!("No auth data found in headers, trying to read from body..."); + read_auth_data_from_body(main_resource, move |auth_result| { + send_auth_result(&auth_result_tx, auth_result) + }); + } + } +} + +fn parse_xml_tag(html: &str, tag: &str) -> Option { + let re = Regex::new(&format!("<{}>(.*)", tag, tag)).unwrap(); + re.captures(html) + .and_then(|captures| captures.get(1)) + .map(|m| m.as_str().to_string()) +} + +pub(crate) async fn clear_webview_cookies(window: &Window) -> anyhow::Result<()> { + let (tx, rx) = oneshot::channel::>(); + + window.with_webview(|wv| { + let send_result = move |result: Result<(), String>| { + if let Err(err) = tx.send(result) { + info!("Failed to send result: {:?}", err); + } + }; + + let wv = wv.inner(); + let context = match wv.context() { + Some(context) => context, + None => { + send_result(Err("No webview context found".into())); + return; + } + }; + let data_manager = match context.website_data_manager() { + Some(manager) => manager, + None => { + send_result(Err("No data manager found".into())); + return; + } + }; + + let now = Instant::now(); + data_manager.clear( + WebsiteDataTypes::COOKIES, + TimeSpan(0), + Cancellable::NONE, + move |result| match result { + Err(err) => { + send_result(Err(err.to_string())); + } + Ok(_) => { + info!("Cookies cleared in {} ms", now.elapsed().as_millis()); + send_result(Ok(())); + } + }, + ); + })?; + + rx.await?.map_err(|err| anyhow::anyhow!(err)) +} diff --git a/apps/gpauth/src/cli.rs b/apps/gpauth/src/cli.rs new file mode 100644 index 0000000..9caf6a2 --- /dev/null +++ b/apps/gpauth/src/cli.rs @@ -0,0 +1,138 @@ +use clap::Parser; +use gpapi::{ + auth::{SamlAuthData, SamlAuthResult}, + utils::{normalize_server, openssl}, + GP_USER_AGENT, +}; +use log::{info, LevelFilter}; +use serde_json::json; +use tauri::{App, AppHandle, RunEvent}; +use tempfile::NamedTempFile; + +use crate::auth_window::{portal_prelogin, AuthWindow}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Parser, Clone)] +#[command(version = VERSION)] +struct Cli { + server: String, + #[arg(long)] + saml_request: Option, + #[arg(long, default_value = GP_USER_AGENT)] + user_agent: String, + #[arg(long)] + hidpi: bool, + #[arg(long)] + fix_openssl: bool, + #[arg(long)] + clean: bool, +} + +impl Cli { + async fn run(&mut self) -> anyhow::Result<()> { + let mut openssl_conf = self.prepare_env()?; + + self.server = normalize_server(&self.server)?; + // Get the initial SAML request + let saml_request = match self.saml_request { + Some(ref saml_request) => saml_request.clone(), + None => portal_prelogin(&self.server, &self.user_agent).await?, + }; + + self.saml_request.replace(saml_request); + + let app = create_app(self.clone())?; + + app.run(move |_app_handle, event| { + if let RunEvent::Exit = event { + if let Some(file) = openssl_conf.take() { + if let Err(err) = file.close() { + info!("Error closing OpenSSL config file: {}", err); + } + } + } + }); + + Ok(()) + } + + fn prepare_env(&self) -> anyhow::Result> { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + + if self.hidpi { + info!("Setting GDK_SCALE=2 and GDK_DPI_SCALE=0.5"); + + std::env::set_var("GDK_SCALE", "2"); + std::env::set_var("GDK_DPI_SCALE", "0.5"); + } + + if self.fix_openssl { + info!("Fixing OpenSSL environment"); + let file = openssl::fix_openssl_env()?; + + return Ok(Some(file)); + } + + Ok(None) + } + + async fn saml_auth(&self, app_handle: AppHandle) -> anyhow::Result { + let auth_window = AuthWindow::new(app_handle) + .server(&self.server) + .user_agent(&self.user_agent) + .saml_request(self.saml_request.as_ref().unwrap()) + .clean(self.clean); + + auth_window.open().await + } +} + +fn create_app(cli: Cli) -> anyhow::Result { + let app = tauri::Builder::default() + .setup(|app| { + let app_handle = app.handle(); + + tauri::async_runtime::spawn(async move { + let auth_result = match cli.saml_auth(app_handle.clone()).await { + Ok(auth_data) => SamlAuthResult::Success(auth_data), + Err(err) => SamlAuthResult::Failure(format!("{}", err)), + }; + + println!("{}", json!(auth_result)); + }); + Ok(()) + }) + .build(tauri::generate_context!())?; + + Ok(app) +} + +fn init_logger() { + env_logger::builder().filter_level(LevelFilter::Info).init(); +} + +pub async fn run() { + let mut cli = Cli::parse(); + + init_logger(); + info!("gpauth started: {}", VERSION); + + if let Err(err) = cli.run().await { + eprintln!("\nError: {}", err); + + if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { + eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); + // Print the command + let args = std::env::args().collect::>(); + eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); + } + + std::process::exit(1); + } +} diff --git a/apps/gpauth/src/main.rs b/apps/gpauth/src/main.rs new file mode 100644 index 0000000..74f86ec --- /dev/null +++ b/apps/gpauth/src/main.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod auth_window; +mod cli; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpauth/tauri.conf.json b/apps/gpauth/tauri.conf.json new file mode 100644 index 0000000..7569835 --- /dev/null +++ b/apps/gpauth/tauri.conf.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://cdn.jsdelivr.net/gh/tauri-apps/tauri@tauri-v1.5.0/tooling/cli/schema.json", + "build": { + "distDir": [ + "index.html" + ], + "devPath": [ + "index.html" + ], + "beforeDevCommand": "", + "beforeBuildCommand": "", + "withGlobalTauri": false + }, + "package": { + "productName": "gpauth", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "http": { + "all": true, + "request": true, + "scope": [ + "http://**", + "https://**" + ] + } + }, + "bundle": { + "active": true, + "targets": "deb", + "identifier": "com.yuezk.gpauth", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "security": { + "csp": null + }, + "windows": [] + } +} diff --git a/apps/gpclient/Cargo.toml b/apps/gpclient/Cargo.toml new file mode 100644 index 0000000..6e38bcc --- /dev/null +++ b/apps/gpclient/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gpclient" +authors.workspace = true +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gpapi = { path = "../../crates/gpapi" } +openconnect = { path = "../../crates/openconnect" } +anyhow.workspace = true +clap.workspace = true +env_logger.workspace = true +inquire = "0.6.2" +log.workspace = true +tokio.workspace = true +sysinfo.workspace = true +serde_json.workspace = true +whoami.workspace = true +tempfile.workspace = true +reqwest.workspace = true +directories = "5.0" +compile-time.workspace = true diff --git a/apps/gpclient/src/cli.rs b/apps/gpclient/src/cli.rs new file mode 100644 index 0000000..b1475a2 --- /dev/null +++ b/apps/gpclient/src/cli.rs @@ -0,0 +1,101 @@ +use clap::{Parser, Subcommand}; +use gpapi::utils::openssl; +use log::{info, LevelFilter}; +use tempfile::NamedTempFile; + +use crate::{ + connect::{ConnectArgs, ConnectHandler}, + disconnect::DisconnectHandler, + launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, +}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Subcommand)] +enum CliCommand { + #[command(about = "Connect to a portal server")] + Connect(ConnectArgs), + #[command(about = "Disconnect from the server")] + Disconnect, + #[command(about = "Launch the GUI")] + LaunchGui(LaunchGuiArgs), +} + +#[derive(Parser)] +#[command( + version = VERSION, + author, + about = "The GlobalProtect VPN client, based on OpenConnect, supports the SSO authentication method.", + help_template = "\ +{before-help}{name} {version} +{author} + +{about} + +{usage-heading} {usage} + +{all-args}{after-help} +" +)] +struct Cli { + #[command(subcommand)] + command: CliCommand, + + #[arg( + long, + help = "Get around the OpenSSL `unsafe legacy renegotiation` error" + )] + fix_openssl: bool, +} + +impl Cli { + fn fix_openssl(&self) -> anyhow::Result> { + if self.fix_openssl { + let file = openssl::fix_openssl_env()?; + return Ok(Some(file)); + } + + Ok(None) + } + + async fn run(&self) -> anyhow::Result<()> { + // The temp file will be dropped automatically when the file handle is dropped + // So, declare it here to ensure it's not dropped + let _file = self.fix_openssl()?; + + match &self.command { + CliCommand::Connect(args) => ConnectHandler::new(args, self.fix_openssl).handle().await, + CliCommand::Disconnect => DisconnectHandler::new().handle(), + CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, + } + } +} + +fn init_logger() { + env_logger::builder().filter_level(LevelFilter::Info).init(); +} + +pub(crate) async fn run() { + let cli = Cli::parse(); + + init_logger(); + info!("gpclient started: {}", VERSION); + + if let Err(err) = cli.run().await { + eprintln!("\nError: {}", err); + + if err.to_string().contains("unsafe legacy renegotiation") && !cli.fix_openssl { + eprintln!("\nRe-run it with the `--fix-openssl` option to work around this issue, e.g.:\n"); + // Print the command + let args = std::env::args().collect::>(); + eprintln!("{} --fix-openssl {}\n", args[0], args[1..].join(" ")); + } + + std::process::exit(1); + } +} diff --git a/apps/gpclient/src/connect.rs b/apps/gpclient/src/connect.rs new file mode 100644 index 0000000..8a02d19 --- /dev/null +++ b/apps/gpclient/src/connect.rs @@ -0,0 +1,150 @@ +use std::{fs, sync::Arc}; + +use clap::Args; +use gpapi::{ + credential::{Credential, PasswordCredential}, + gateway::gateway_login, + gp_params::GpParams, + portal::{prelogin, retrieve_config, Prelogin}, + process::auth_launcher::SamlAuthLauncher, + utils::{self, shutdown_signal}, + GP_USER_AGENT, +}; +use inquire::{Password, PasswordDisplayMode, Select, Text}; +use log::info; +use openconnect::Vpn; + +use crate::GP_CLIENT_LOCK_FILE; + +#[derive(Args)] +pub(crate) struct ConnectArgs { + #[arg(help = "The portal server to connect to")] + server: String, + #[arg( + short, + long, + help = "The gateway to connect to, it will prompt if not specified" + )] + gateway: Option, + #[arg( + short, + long, + help = "The username to use, it will prompt if not specified" + )] + user: Option, + #[arg(long, short, help = "The VPNC script to use")] + script: Option, + #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] + user_agent: String, + #[arg(long, help = "The HiDPI mode, useful for high resolution screens")] + hidpi: bool, + #[arg(long, help = "Do not reuse the remembered authentication cookie")] + clean: bool, +} + +pub(crate) struct ConnectHandler<'a> { + args: &'a ConnectArgs, + fix_openssl: bool, +} + +impl<'a> ConnectHandler<'a> { + pub(crate) fn new(args: &'a ConnectArgs, fix_openssl: bool) -> Self { + Self { args, fix_openssl } + } + + pub(crate) async fn handle(&self) -> anyhow::Result<()> { + let portal = utils::normalize_server(self.args.server.as_str())?; + + let gp_params = GpParams::builder() + .user_agent(&self.args.user_agent) + .build(); + + let prelogin = prelogin(&portal, &self.args.user_agent).await?; + let portal_credential = self.obtain_portal_credential(&prelogin).await?; + let mut portal_config = retrieve_config(&portal, &portal_credential, &gp_params).await?; + + let selected_gateway = match &self.args.gateway { + Some(gateway) => portal_config + .find_gateway(gateway) + .ok_or_else(|| anyhow::anyhow!("Cannot find gateway {}", gateway))?, + None => { + portal_config.sort_gateways(prelogin.region()); + let gateways = portal_config.gateways(); + + if gateways.len() > 1 { + Select::new("Which gateway do you want to connect to?", gateways) + .with_vim_mode(true) + .prompt()? + } else { + gateways[0] + } + } + }; + + let gateway = selected_gateway.server(); + let cred = portal_config.auth_cookie().into(); + let token = gateway_login(gateway, &cred, &gp_params).await?; + + let vpn = Vpn::builder(gateway, &token) + .user_agent(self.args.user_agent.clone()) + .script(self.args.script.clone()) + .build(); + + let vpn = Arc::new(vpn); + let vpn_clone = vpn.clone(); + + // Listen for the interrupt signal in the background + tokio::spawn(async move { + shutdown_signal().await; + info!("Received the interrupt signal, disconnecting..."); + vpn_clone.disconnect(); + }); + + vpn.connect(write_pid_file); + + if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() { + info!("Removing PID file"); + fs::remove_file(GP_CLIENT_LOCK_FILE)?; + } + + Ok(()) + } + + async fn obtain_portal_credential(&self, prelogin: &Prelogin) -> anyhow::Result { + match prelogin { + Prelogin::Saml(prelogin) => { + SamlAuthLauncher::new(&self.args.server) + .user_agent(&self.args.user_agent) + .saml_request(prelogin.saml_request()) + .hidpi(self.args.hidpi) + .fix_openssl(self.fix_openssl) + .clean(self.args.clean) + .launch() + .await + } + Prelogin::Standard(prelogin) => { + println!("{}", prelogin.auth_message()); + + let user = self.args.user.as_ref().map_or_else( + || Text::new(&format!("{}:", prelogin.label_username())).prompt(), + |user| Ok(user.to_owned()), + )?; + let password = Password::new(&format!("{}:", prelogin.label_password())) + .without_confirmation() + .with_display_mode(PasswordDisplayMode::Masked) + .prompt()?; + + let password_cred = PasswordCredential::new(&user, &password); + + Ok(password_cred.into()) + } + } + } +} + +fn write_pid_file() { + let pid = std::process::id(); + + fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap(); + info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE); +} diff --git a/apps/gpclient/src/disconnect.rs b/apps/gpclient/src/disconnect.rs new file mode 100644 index 0000000..7318d61 --- /dev/null +++ b/apps/gpclient/src/disconnect.rs @@ -0,0 +1,31 @@ +use crate::GP_CLIENT_LOCK_FILE; +use log::{info, warn}; +use std::fs; +use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt}; + +pub(crate) struct DisconnectHandler; + +impl DisconnectHandler { + pub(crate) fn new() -> Self { + Self + } + + pub(crate) fn handle(&self) -> anyhow::Result<()> { + if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() { + warn!("PID file not found, maybe the client is not running"); + return Ok(()); + } + + let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?; + let pid = pid.trim().parse::()?; + let s = System::new_all(); + + if let Some(process) = s.process(Pid::from(pid)) { + info!("Found process {}, killing...", pid); + if process.kill_with(Signal::Interrupt).is_none() { + warn!("Failed to kill process {}", pid); + } + } + Ok(()) + } +} diff --git a/apps/gpclient/src/launch_gui.rs b/apps/gpclient/src/launch_gui.rs new file mode 100644 index 0000000..e8f8468 --- /dev/null +++ b/apps/gpclient/src/launch_gui.rs @@ -0,0 +1,88 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use clap::Args; +use directories::ProjectDirs; +use gpapi::{ + process::service_launcher::ServiceLauncher, + utils::{endpoint::http_endpoint, env_file, shutdown_signal}, +}; +use log::info; + +#[derive(Args)] +pub(crate) struct LaunchGuiArgs { + #[clap(long, help = "Launch the GUI minimized")] + minimized: bool, +} + +pub(crate) struct LaunchGuiHandler<'a> { + args: &'a LaunchGuiArgs, +} + +impl<'a> LaunchGuiHandler<'a> { + pub(crate) fn new(args: &'a LaunchGuiArgs) -> Self { + Self { args } + } + + pub(crate) async fn handle(&self) -> anyhow::Result<()> { + // `launch-gui`cannot be run as root + let user = whoami::username(); + if user == "root" { + anyhow::bail!("`launch-gui` cannot be run as root"); + } + + if try_active_gui().await.is_ok() { + info!("The GUI is already running"); + return Ok(()); + } + + tokio::spawn(async move { + shutdown_signal().await; + info!("Shutting down..."); + }); + + let log_file = get_log_file()?; + let log_file_path = log_file.to_string_lossy().to_string(); + + info!("Log file: {}", log_file_path); + + let mut extra_envs = HashMap::::new(); + extra_envs.insert("GP_LOG_FILE".into(), log_file_path.clone()); + + // Persist the environment variables to a file + let env_file = env_file::persist_env_vars(Some(extra_envs))?; + let env_file = env_file.into_temp_path(); + let env_file_path = env_file.to_string_lossy().to_string(); + + let exit_status = ServiceLauncher::new() + .minimized(self.args.minimized) + .env_file(&env_file_path) + .log_file(&log_file_path) + .launch() + .await?; + + info!("Service exited with status: {}", exit_status); + + Ok(()) + } +} + +async fn try_active_gui() -> anyhow::Result<()> { + let service_endpoint = http_endpoint().await?; + + reqwest::Client::default() + .post(format!("{}/active-gui", service_endpoint)) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +pub fn get_log_file() -> anyhow::Result { + let dirs = ProjectDirs::from("com.yuezk", "GlobalProtect-openconnect", "gpclient") + .ok_or_else(|| anyhow::anyhow!("Failed to get project dirs"))?; + + fs::create_dir_all(dirs.data_dir())?; + + Ok(dirs.data_dir().join("gpclient.log")) +} diff --git a/apps/gpclient/src/main.rs b/apps/gpclient/src/main.rs new file mode 100644 index 0000000..41343ed --- /dev/null +++ b/apps/gpclient/src/main.rs @@ -0,0 +1,11 @@ +mod cli; +mod connect; +mod disconnect; +mod launch_gui; + +pub(crate) const GP_CLIENT_LOCK_FILE: &str = "/var/run/gpclient.lock"; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpservice/Cargo.toml b/apps/gpservice/Cargo.toml new file mode 100644 index 0000000..1ac8ead --- /dev/null +++ b/apps/gpservice/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "gpservice" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gpapi = { path = "../../crates/gpapi" } +openconnect = { path = "../../crates/openconnect" } +clap.workspace = true +anyhow.workspace = true +tokio.workspace = true +tokio-util.workspace = true +axum = { workspace = true, features = ["ws"] } +futures.workspace = true +serde_json.workspace = true +env_logger.workspace = true +log.workspace = true +compile-time.workspace = true diff --git a/apps/gpservice/com.yuezk.gpservice.policy b/apps/gpservice/com.yuezk.gpservice.policy new file mode 100644 index 0000000..3c2128f --- /dev/null +++ b/apps/gpservice/com.yuezk.gpservice.policy @@ -0,0 +1,19 @@ + + + + GlobalProtect-openconnect + https://github.com/yuezk/GlobalProtect-openconnect + gpgui + + Run GPService as root + Authentication is required to run the GPService as root + + yes + yes + yes + + /home/kevin/Documents/repos/gp/target/debug/gpservice + --with-gui + true + + diff --git a/apps/gpservice/src/cli.rs b/apps/gpservice/src/cli.rs new file mode 100644 index 0000000..9c0435e --- /dev/null +++ b/apps/gpservice/src/cli.rs @@ -0,0 +1,182 @@ +use std::sync::Arc; +use std::{collections::HashMap, io::Write}; + +use anyhow::bail; +use clap::Parser; +use gpapi::{ + process::gui_launcher::GuiLauncher, + service::{request::WsRequest, vpn_state::VpnState}, + utils::{ + crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal, + }, + GP_SERVICE_LOCK_FILE, +}; +use log::{info, warn, LevelFilter}; +use tokio::sync::{mpsc, watch}; + +use crate::{vpn_task::VpnTask, ws_server::WsServer}; + +const VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + compile_time::date_str!(), + ")" +); + +#[derive(Parser)] +#[command(version = VERSION)] +struct Cli { + #[clap(long)] + minimized: bool, + #[clap(long)] + env_file: Option, + #[cfg(debug_assertions)] + #[clap(long)] + no_gui: bool, +} + +impl Cli { + async fn run(&mut self, redaction: Arc) -> anyhow::Result<()> { + let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE)); + + if lock_file.check_health().await { + bail!("Another instance of the service is already running"); + } + + let api_key = self.prepare_api_key(); + + // Channel for sending requests to the VPN task + let (ws_req_tx, ws_req_rx) = mpsc::channel::(32); + // Channel for receiving the VPN state from the VPN task + let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected); + + let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx); + let ws_server = WsServer::new( + api_key.clone(), + ws_req_tx, + vpn_state_rx, + lock_file.clone(), + redaction, + ); + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); + let shutdown_tx_clone = shutdown_tx.clone(); + let vpn_task_token = vpn_task.cancel_token(); + let server_token = ws_server.cancel_token(); + + let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await }); + let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await }); + + #[cfg(debug_assertions)] + let no_gui = self.no_gui; + + #[cfg(not(debug_assertions))] + let no_gui = false; + + if no_gui { + info!("GUI is disabled"); + } else { + let envs = self + .env_file + .as_ref() + .map(env_file::load_env_vars) + .transpose()?; + + let minimized = self.minimized; + + tokio::spawn(async move { + launch_gui(envs, api_key, minimized).await; + let _ = shutdown_tx.send(()).await; + }); + } + + tokio::select! { + _ = shutdown_signal() => { + info!("Shutdown signal received"); + } + _ = shutdown_rx.recv() => { + info!("Shutdown request received, shutting down"); + } + } + + vpn_task_token.cancel(); + let _ = tokio::join!(vpn_task_handle, ws_server_handle); + + lock_file.unlock()?; + + info!("gpservice stopped"); + + Ok(()) + } + + fn prepare_api_key(&self) -> Vec { + #[cfg(debug_assertions)] + if self.no_gui { + return gpapi::GP_API_KEY.to_vec(); + } + + generate_key().to_vec() + } +} + +fn init_logger() -> Arc { + let redaction = Arc::new(Redaction::new()); + let redaction_clone = Arc::clone(&redaction); + // let target = Box::new(File::create("log.txt").expect("Can't create file")); + env_logger::builder() + .filter_level(LevelFilter::Info) + .format(move |buf, record| { + let timestamp = buf.timestamp(); + writeln!( + buf, + "[{} {} {}] {}", + timestamp, + record.level(), + record.module_path().unwrap_or_default(), + redaction_clone.redact_str(&record.args().to_string()) + ) + }) + // .target(env_logger::Target::Pipe(target)) + .init(); + + redaction +} + +async fn launch_gui(envs: Option>, api_key: Vec, mut minimized: bool) { + loop { + let api_key_clone = api_key.clone(); + let gui_launcher = GuiLauncher::new() + .envs(envs.clone()) + .api_key(api_key_clone) + .minimized(minimized); + + match gui_launcher.launch().await { + Ok(exit_status) => { + // Exit code 99 means that the GUI needs to be restarted + if exit_status.code() != Some(99) { + info!("GUI exited with code {:?}", exit_status.code()); + break; + } + + info!("GUI exited with code 99, restarting"); + minimized = false; + } + Err(err) => { + warn!("Failed to launch GUI: {}", err); + break; + } + } + } +} + +pub async fn run() { + let mut cli = Cli::parse(); + + let redaction = init_logger(); + info!("gpservice started: {}", VERSION); + + if let Err(e) = cli.run(redaction).await { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} diff --git a/apps/gpservice/src/handlers.rs b/apps/gpservice/src/handlers.rs new file mode 100644 index 0000000..4f90b06 --- /dev/null +++ b/apps/gpservice/src/handlers.rs @@ -0,0 +1,94 @@ +use std::{borrow::Cow, ops::ControlFlow, sync::Arc}; + +use axum::{ + extract::{ + ws::{self, CloseFrame, Message, WebSocket}, + State, WebSocketUpgrade, + }, + response::IntoResponse, +}; +use futures::{SinkExt, StreamExt}; +use gpapi::service::event::WsEvent; +use log::{info, warn}; + +use crate::ws_server::WsServerContext; + +pub(crate) async fn health() -> impl IntoResponse { + "OK" +} + +pub(crate) async fn active_gui(State(ctx): State>) -> impl IntoResponse { + ctx.send_event(WsEvent::ActiveGui).await; +} + +pub(crate) async fn ws_handler( + ws: WebSocketUpgrade, + State(ctx): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, ctx)) +} + +async fn handle_socket(mut socket: WebSocket, ctx: Arc) { + // Send ping message + if let Err(err) = socket.send(Message::Ping("Hi".into())).await { + warn!("Failed to send ping: {}", err); + return; + } + + // Wait for pong message + if socket.recv().await.is_none() { + warn!("Failed to receive pong"); + return; + } + + info!("New client connected"); + + let (mut sender, mut receiver) = socket.split(); + let (connection, mut msg_rx) = ctx.add_connection().await; + + let send_task = tokio::spawn(async move { + while let Some(msg) = msg_rx.recv().await { + if let Err(err) = sender.send(msg).await { + info!("Failed to send message: {}", err); + break; + } + } + + let close_msg = Message::Close(Some(CloseFrame { + code: ws::close_code::NORMAL, + reason: Cow::from("Goodbye"), + })); + + if let Err(err) = sender.send(close_msg).await { + warn!("Failed to close socket: {}", err); + } + }); + + let conn = Arc::clone(&connection); + let ctx_clone = Arc::clone(&ctx); + let recv_task = tokio::spawn(async move { + while let Some(Ok(msg)) = receiver.next().await { + let ControlFlow::Continue(ws_req) = conn.recv_msg(msg) else { + break; + }; + + if let Err(err) = ctx_clone.forward_req(ws_req).await { + info!("Failed to forward request: {}", err); + break; + } + } + }); + + tokio::select! { + _ = send_task => { + info!("WS server send task completed"); + }, + _ = recv_task => { + info!("WS server recv task completed"); + } + } + + info!("Client disconnected"); + + ctx.remove_connection(connection).await; +} diff --git a/apps/gpservice/src/main.rs b/apps/gpservice/src/main.rs new file mode 100644 index 0000000..aa7daa3 --- /dev/null +++ b/apps/gpservice/src/main.rs @@ -0,0 +1,11 @@ +mod cli; +mod handlers; +mod routes; +mod vpn_task; +mod ws_server; +mod ws_connection; + +#[tokio::main] +async fn main() { + cli::run().await; +} diff --git a/apps/gpservice/src/routes.rs b/apps/gpservice/src/routes.rs new file mode 100644 index 0000000..780b11f --- /dev/null +++ b/apps/gpservice/src/routes.rs @@ -0,0 +1,13 @@ +use std::sync::Arc; + +use axum::{routing::{get, post}, Router}; + +use crate::{handlers, ws_server::WsServerContext}; + +pub(crate) fn routes(ctx: Arc) -> Router { + Router::new() + .route("/health", get(handlers::health)) + .route("/active-gui", post(handlers::active_gui)) + .route("/ws", get(handlers::ws_handler)) + .with_state(ctx) +} diff --git a/apps/gpservice/src/vpn_task.rs b/apps/gpservice/src/vpn_task.rs new file mode 100644 index 0000000..55424a1 --- /dev/null +++ b/apps/gpservice/src/vpn_task.rs @@ -0,0 +1,144 @@ +use std::{sync::Arc, thread}; + +use gpapi::service::{ + request::{ConnectRequest, WsRequest}, + vpn_state::VpnState, +}; +use log::info; +use openconnect::Vpn; +use tokio::sync::{mpsc, oneshot, watch, RwLock}; +use tokio_util::sync::CancellationToken; + +pub(crate) struct VpnTaskContext { + vpn_handle: Arc>>, + vpn_state_tx: Arc>, + disconnect_rx: RwLock>>, +} + +impl VpnTaskContext { + pub fn new(vpn_state_tx: watch::Sender) -> Self { + Self { + vpn_handle: Default::default(), + vpn_state_tx: Arc::new(vpn_state_tx), + disconnect_rx: Default::default(), + } + } + + pub async fn connect(&self, req: ConnectRequest) { + let vpn_state = self.vpn_state_tx.borrow().clone(); + if !matches!(vpn_state, VpnState::Disconnected) { + info!("VPN is not disconnected, ignore the request"); + return; + } + + let info = req.info().clone(); + let vpn_handle = self.vpn_handle.clone(); + let args = req.args(); + let vpn = Vpn::builder(req.gateway().server(), args.cookie()) + .user_agent(args.user_agent()) + .script(args.vpnc_script()) + .os(args.openconnect_os()) + .build(); + + // Save the VPN handle + vpn_handle.write().await.replace(vpn); + + let vpn_state_tx = self.vpn_state_tx.clone(); + let connect_info = Box::new(info.clone()); + vpn_state_tx.send(VpnState::Connecting(connect_info)).ok(); + + let (disconnect_tx, disconnect_rx) = oneshot::channel::<()>(); + self.disconnect_rx.write().await.replace(disconnect_rx); + + // Spawn a new thread to process the VPN connection, cannot use tokio::spawn here. + // Otherwise, it will block the tokio runtime and cannot send the VPN state to the channel + thread::spawn(move || { + let vpn_state_tx_clone = vpn_state_tx.clone(); + + vpn_handle.blocking_read().as_ref().map(|vpn| { + vpn.connect(move || { + let connect_info = Box::new(info.clone()); + vpn_state_tx.send(VpnState::Connected(connect_info)).ok(); + }) + }); + + // Notify the VPN is disconnected + vpn_state_tx_clone.send(VpnState::Disconnected).ok(); + // Remove the VPN handle + vpn_handle.blocking_write().take(); + + disconnect_tx.send(()).ok(); + }); + } + + pub async fn disconnect(&self) { + if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { + if let Some(vpn) = self.vpn_handle.read().await.as_ref() { + self.vpn_state_tx.send(VpnState::Disconnecting).ok(); + vpn.disconnect() + } + // Wait for the VPN to be disconnected + disconnect_rx.await.ok(); + info!("VPN disconnected"); + } else { + info!("VPN is not connected, skip disconnect"); + self.vpn_state_tx.send(VpnState::Disconnected).ok(); + } + } +} + +pub(crate) struct VpnTask { + ws_req_rx: mpsc::Receiver, + ctx: Arc, + cancel_token: CancellationToken, +} + +impl VpnTask { + pub fn new(ws_req_rx: mpsc::Receiver, vpn_state_tx: watch::Sender) -> Self { + let ctx = Arc::new(VpnTaskContext::new(vpn_state_tx)); + let cancel_token = CancellationToken::new(); + + Self { + ws_req_rx, + ctx, + cancel_token, + } + } + + pub fn cancel_token(&self) -> CancellationToken { + self.cancel_token.clone() + } + + pub async fn start(&mut self, server_cancel_token: CancellationToken) { + let cancel_token = self.cancel_token.clone(); + + tokio::select! { + _ = self.recv() => { + info!("VPN task stopped"); + } + _ = cancel_token.cancelled() => { + info!("VPN task cancelled"); + self.ctx.disconnect().await; + } + } + + server_cancel_token.cancel(); + } + + async fn recv(&mut self) { + while let Some(req) = self.ws_req_rx.recv().await { + tokio::spawn(process_ws_req(req, self.ctx.clone())); + } + } +} + +async fn process_ws_req(req: WsRequest, ctx: Arc) { + match req { + WsRequest::Connect(req) => { + ctx.connect(*req).await; + } + WsRequest::Disconnect(_) => { + ctx.disconnect().await; + } + } +} diff --git a/apps/gpservice/src/ws_connection.rs b/apps/gpservice/src/ws_connection.rs new file mode 100644 index 0000000..13c7f6c --- /dev/null +++ b/apps/gpservice/src/ws_connection.rs @@ -0,0 +1,53 @@ +use std::{ops::ControlFlow, sync::Arc}; + +use axum::extract::ws::{CloseFrame, Message}; +use gpapi::{ + service::{event::WsEvent, request::WsRequest}, + utils::crypto::Crypto, +}; +use log::{info, warn}; +use tokio::sync::mpsc; + +pub(crate) struct WsConnection { + crypto: Arc, + tx: mpsc::Sender, +} + +impl WsConnection { + pub fn new(crypto: Arc, tx: mpsc::Sender) -> Self { + Self { crypto, tx } + } + + pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> { + let encrypted = self.crypto.encrypt(event)?; + let msg = Message::Binary(encrypted); + + self.tx.send(msg).await?; + + Ok(()) + } + + pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> { + match msg { + Message::Binary(data) => match self.crypto.decrypt(data) { + Ok(ws_req) => ControlFlow::Continue(ws_req), + Err(err) => { + info!("Failed to decrypt message: {}", err); + ControlFlow::Break(()) + } + }, + Message::Close(cf) => { + if let Some(CloseFrame { code, reason }) = cf { + info!("Client sent close, code {} and reason `{}`", code, reason); + } else { + info!("Client somehow sent close message without CloseFrame"); + } + ControlFlow::Break(()) + } + _ => { + warn!("WS server received unexpected message: {:?}", msg); + ControlFlow::Break(()) + } + } + } +} diff --git a/apps/gpservice/src/ws_server.rs b/apps/gpservice/src/ws_server.rs new file mode 100644 index 0000000..8f09542 --- /dev/null +++ b/apps/gpservice/src/ws_server.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use axum::extract::ws::Message; +use gpapi::{ + service::{event::WsEvent, request::WsRequest, vpn_state::VpnState}, + utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction}, +}; +use log::{info, warn}; +use tokio::{ + net::TcpListener, + sync::{mpsc, watch, RwLock}, +}; +use tokio_util::sync::CancellationToken; + +use crate::{routes, ws_connection::WsConnection}; + +pub(crate) struct WsServerContext { + crypto: Arc, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + redaction: Arc, + connections: RwLock>>, +} + +impl WsServerContext { + pub fn new( + api_key: Vec, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + redaction: Arc, + ) -> Self { + Self { + crypto: Arc::new(Crypto::new(api_key)), + ws_req_tx, + vpn_state_rx, + redaction, + connections: Default::default(), + } + } + + pub async fn send_event(&self, event: WsEvent) { + let connections = self.connections.read().await; + + for conn in connections.iter() { + let _ = conn.send_event(&event).await; + } + } + + pub async fn add_connection(&self) -> (Arc, mpsc::Receiver) { + let (tx, rx) = mpsc::channel::(32); + let conn = Arc::new(WsConnection::new(Arc::clone(&self.crypto), tx)); + + // Send current VPN state to new client + info!("Sending current VPN state to new client"); + let vpn_state = self.vpn_state_rx.borrow().clone(); + if let Err(err) = conn.send_event(&WsEvent::VpnState(vpn_state)).await { + warn!("Failed to send VPN state to new client: {}", err); + } + + self.connections.write().await.push(Arc::clone(&conn)); + + (conn, rx) + } + + pub async fn remove_connection(&self, conn: Arc) { + let mut connections = self.connections.write().await; + connections.retain(|c| !Arc::ptr_eq(c, &conn)); + } + + fn vpn_state_rx(&self) -> watch::Receiver { + self.vpn_state_rx.clone() + } + + pub async fn forward_req(&self, req: WsRequest) -> anyhow::Result<()> { + if let WsRequest::Connect(ref req) = req { + self + .redaction + .add_values(&[req.gateway().server(), req.args().cookie()])? + } + + self.ws_req_tx.send(req).await?; + + Ok(()) + } +} + +pub(crate) struct WsServer { + ctx: Arc, + cancel_token: CancellationToken, + lock_file: Arc, +} + +impl WsServer { + pub fn new( + api_key: Vec, + ws_req_tx: mpsc::Sender, + vpn_state_rx: watch::Receiver, + lock_file: Arc, + redaction: Arc, + ) -> Self { + let ctx = Arc::new(WsServerContext::new( + api_key, + ws_req_tx, + vpn_state_rx, + redaction, + )); + let cancel_token = CancellationToken::new(); + + Self { + ctx, + cancel_token, + lock_file, + } + } + + pub fn cancel_token(&self) -> CancellationToken { + self.cancel_token.clone() + } + + pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) { + if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await { + let local_addr = listener.local_addr().unwrap(); + + self.lock_file.lock(local_addr.port().to_string()).unwrap(); + + info!("WS server listening on port: {}", local_addr.port()); + + tokio::select! { + _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => { + info!("VPN state watch task completed"); + } + _ = start_server(listener, self.ctx.clone()) => { + info!("WS server stopped"); + } + _ = self.cancel_token.cancelled() => { + info!("WS server cancelled"); + } + } + } + + let _ = shutdown_tx.send(()).await; + } +} + +async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver, ctx: Arc) { + while vpn_state_rx.changed().await.is_ok() { + let vpn_state = vpn_state_rx.borrow().clone(); + ctx.send_event(WsEvent::VpnState(vpn_state)).await; + } +} + +async fn start_server(listener: TcpListener, ctx: Arc) -> anyhow::Result<()> { + let routes = routes::routes(ctx); + + axum::serve(listener, routes).await?; + + Ok(()) +} diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml new file mode 100644 index 0000000..b5f252a --- /dev/null +++ b/crates/gpapi/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "gpapi" +version.workspace = true +edition.workspace = true +license = "MIT" + +[dependencies] +anyhow.workspace = true +base64.workspace = true +log.workspace = true +reqwest.workspace = true +roxmltree.workspace = true +serde.workspace = true +specta.workspace = true +specta-macros.workspace = true +urlencoding.workspace = true +tokio.workspace = true +serde_json.workspace = true +whoami.workspace = true +tempfile.workspace = true +thiserror.workspace = true +chacha20poly1305 = { version = "0.10", features = ["std"] } +redact-engine.workspace = true +url.workspace = true +regex.workspace = true +dotenvy_macro.workspace = true +users.workspace = true + +tauri = { workspace = true, optional = true } + +[features] +tauri = ["dep:tauri"] diff --git a/crates/gpapi/src/auth.rs b/crates/gpapi/src/auth.rs new file mode 100644 index 0000000..0e44666 --- /dev/null +++ b/crates/gpapi/src/auth.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SamlAuthData { + username: String, + prelogin_cookie: Option, + portal_userauthcookie: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SamlAuthResult { + Success(SamlAuthData), + Failure(String), +} + +impl SamlAuthResult { + pub fn is_success(&self) -> bool { + match self { + SamlAuthResult::Success(_) => true, + SamlAuthResult::Failure(_) => false, + } + } +} + +impl SamlAuthData { + pub fn new( + username: String, + prelogin_cookie: Option, + portal_userauthcookie: Option, + ) -> Self { + Self { + username, + prelogin_cookie, + portal_userauthcookie, + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn prelogin_cookie(&self) -> Option<&str> { + self.prelogin_cookie.as_deref() + } + + pub fn check( + username: &Option, + prelogin_cookie: &Option, + portal_userauthcookie: &Option, + ) -> bool { + let username_valid = username + .as_ref() + .is_some_and(|username| !username.is_empty()); + let prelogin_cookie_valid = prelogin_cookie.as_ref().is_some_and(|val| val.len() > 5); + let portal_userauthcookie_valid = portal_userauthcookie + .as_ref() + .is_some_and(|val| val.len() > 5); + + username_valid && (prelogin_cookie_valid || portal_userauthcookie_valid) + } +} diff --git a/crates/gpapi/src/credential.rs b/crates/gpapi/src/credential.rs new file mode 100644 index 0000000..b3b736b --- /dev/null +++ b/crates/gpapi/src/credential.rs @@ -0,0 +1,223 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::auth::SamlAuthData; + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PasswordCredential { + username: String, + password: String, +} + +impl PasswordCredential { + pub fn new(username: &str, password: &str) -> Self { + Self { + username: username.to_string(), + password: password.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> &str { + &self.password + } +} + +impl From<&CachedCredential> for PasswordCredential { + fn from(value: &CachedCredential) -> Self { + Self::new(value.username(), value.password().unwrap_or_default()) + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PreloginCookieCredential { + username: String, + prelogin_cookie: String, +} + +impl PreloginCookieCredential { + pub fn new(username: &str, prelogin_cookie: &str) -> Self { + Self { + username: username.to_string(), + prelogin_cookie: prelogin_cookie.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn prelogin_cookie(&self) -> &str { + &self.prelogin_cookie + } +} + +impl TryFrom for PreloginCookieCredential { + type Error = anyhow::Error; + + fn try_from(value: SamlAuthData) -> Result { + let username = value.username().to_string(); + let prelogin_cookie = value + .prelogin_cookie() + .ok_or_else(|| anyhow::anyhow!("Missing prelogin cookie"))? + .to_string(); + + Ok(Self::new(&username, &prelogin_cookie)) + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AuthCookieCredential { + username: String, + user_auth_cookie: String, + prelogon_user_auth_cookie: String, +} + +impl AuthCookieCredential { + pub fn new(username: &str, user_auth_cookie: &str, prelogon_user_auth_cookie: &str) -> Self { + Self { + username: username.to_string(), + user_auth_cookie: user_auth_cookie.to_string(), + prelogon_user_auth_cookie: prelogon_user_auth_cookie.to_string(), + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn user_auth_cookie(&self) -> &str { + &self.user_auth_cookie + } + + pub fn prelogon_user_auth_cookie(&self) -> &str { + &self.prelogon_user_auth_cookie + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CachedCredential { + username: String, + password: Option, + auth_cookie: AuthCookieCredential, +} + +impl CachedCredential { + pub fn new( + username: String, + password: Option, + auth_cookie: AuthCookieCredential, + ) -> Self { + Self { + username, + password, + auth_cookie, + } + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> Option<&str> { + self.password.as_deref() + } + + pub fn auth_cookie(&self) -> &AuthCookieCredential { + &self.auth_cookie + } + + pub fn set_auth_cookie(&mut self, auth_cookie: AuthCookieCredential) { + self.auth_cookie = auth_cookie; + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Credential { + Password(PasswordCredential), + PreloginCookie(PreloginCookieCredential), + AuthCookie(AuthCookieCredential), + CachedCredential(CachedCredential), +} + +impl Credential { + pub fn username(&self) -> &str { + match self { + Credential::Password(cred) => cred.username(), + Credential::PreloginCookie(cred) => cred.username(), + Credential::AuthCookie(cred) => cred.username(), + Credential::CachedCredential(cred) => cred.username(), + } + } + + pub fn to_params(&self) -> HashMap<&str, &str> { + let mut params = HashMap::new(); + params.insert("user", self.username()); + + match self { + Credential::Password(cred) => { + params.insert("passwd", cred.password()); + } + Credential::PreloginCookie(cred) => { + params.insert("prelogin-cookie", cred.prelogin_cookie()); + } + Credential::AuthCookie(cred) => { + params.insert("portal-userauthcookie", cred.user_auth_cookie()); + params.insert( + "portal-prelogonuserauthcookie", + cred.prelogon_user_auth_cookie(), + ); + } + Credential::CachedCredential(cred) => { + if let Some(password) = cred.password() { + params.insert("passwd", password); + } + params.insert("portal-userauthcookie", cred.auth_cookie.user_auth_cookie()); + params.insert( + "portal-prelogonuserauthcookie", + cred.auth_cookie.prelogon_user_auth_cookie(), + ); + } + } + + params + } +} + +impl TryFrom for Credential { + type Error = anyhow::Error; + + fn try_from(value: SamlAuthData) -> Result { + let prelogin_cookie = PreloginCookieCredential::try_from(value)?; + + Ok(Self::PreloginCookie(prelogin_cookie)) + } +} + +impl From for Credential { + fn from(value: PasswordCredential) -> Self { + Self::Password(value) + } +} + +impl From<&AuthCookieCredential> for Credential { + fn from(value: &AuthCookieCredential) -> Self { + Self::AuthCookie(value.clone()) + } +} + +impl From<&CachedCredential> for Credential { + fn from(value: &CachedCredential) -> Self { + Self::CachedCredential(value.clone()) + } +} diff --git a/crates/gpapi/src/gateway/login.rs b/crates/gpapi/src/gateway/login.rs new file mode 100644 index 0000000..04a9b98 --- /dev/null +++ b/crates/gpapi/src/gateway/login.rs @@ -0,0 +1,74 @@ +use log::info; +use reqwest::Client; +use roxmltree::Document; +use urlencoding::encode; + +use crate::{credential::Credential, gp_params::GpParams}; + +pub async fn gateway_login( + gateway: &str, + cred: &Credential, + gp_params: &GpParams, +) -> anyhow::Result { + let login_url = format!("https://{}/ssl-vpn/login.esp", gateway); + let client = Client::builder() + .user_agent(gp_params.user_agent()) + .build()?; + + let mut params = cred.to_params(); + let extra_params = gp_params.to_params(); + + params.extend(extra_params); + params.insert("server", gateway); + + info!("Gateway login, user_agent: {}", gp_params.user_agent()); + + let res_xml = client + .post(&login_url) + .form(¶ms) + .send() + .await? + .error_for_status()? + .text() + .await?; + + let doc = Document::parse(&res_xml)?; + + build_gateway_token(&doc, gp_params.computer()) +} + +fn build_gateway_token(doc: &Document, computer: &str) -> anyhow::Result { + let args = doc + .descendants() + .filter(|n| n.has_tag_name("argument")) + .map(|n| n.text().unwrap_or("").to_string()) + .collect::>(); + + let params = [ + read_args(&args, 1, "authcookie")?, + read_args(&args, 3, "portal")?, + read_args(&args, 4, "user")?, + read_args(&args, 7, "domain")?, + read_args(&args, 15, "preferred-ip")?, + ("computer", computer), + ]; + + let token = params + .iter() + .map(|(k, v)| format!("{}={}", k, encode(v))) + .collect::>() + .join("&"); + + Ok(token) +} + +fn read_args<'a>( + args: &'a [String], + index: usize, + key: &'a str, +) -> anyhow::Result<(&'a str, &'a str)> { + args + .get(index) + .ok_or_else(|| anyhow::anyhow!("Failed to read {key} from args")) + .map(|s| (key, s.as_ref())) +} diff --git a/crates/gpapi/src/gateway/mod.rs b/crates/gpapi/src/gateway/mod.rs new file mode 100644 index 0000000..7db09bc --- /dev/null +++ b/crates/gpapi/src/gateway/mod.rs @@ -0,0 +1,41 @@ +mod login; +mod parse_gateways; + +pub use login::*; +pub(crate) use parse_gateways::*; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use std::fmt::Display; + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +pub(crate) struct PriorityRule { + pub(crate) name: String, + pub(crate) priority: u32, +} + +#[derive(Debug, Serialize, Deserialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Gateway { + pub(crate) name: String, + pub(crate) address: String, + pub(crate) priority: u32, + pub(crate) priority_rules: Vec, +} + +impl Display for Gateway { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.name, self.address) + } +} + +impl Gateway { + pub fn name(&self) -> &str { + &self.name + } + + pub fn server(&self) -> &str { + &self.address + } +} diff --git a/crates/gpapi/src/gateway/parse_gateways.rs b/crates/gpapi/src/gateway/parse_gateways.rs new file mode 100644 index 0000000..384c382 --- /dev/null +++ b/crates/gpapi/src/gateway/parse_gateways.rs @@ -0,0 +1,63 @@ +use roxmltree::Document; + +use super::{Gateway, PriorityRule}; + +pub(crate) fn parse_gateways(doc: &Document) -> Option> { + let node_gateways = doc.descendants().find(|n| n.has_tag_name("gateways"))?; + let list_gateway = node_gateways + .descendants() + .find(|n| n.has_tag_name("list"))?; + + let gateways = list_gateway + .children() + .filter_map(|gateway_item| { + if !gateway_item.has_tag_name("entry") { + return None; + } + let address = gateway_item.attribute("name").unwrap_or("").to_string(); + let name = gateway_item + .children() + .find(|n| n.has_tag_name("description")) + .and_then(|n| n.text()) + .unwrap_or("") + .to_string(); + let priority = gateway_item + .children() + .find(|n| n.has_tag_name("priority")) + .and_then(|n| n.text()) + .and_then(|s| s.parse().ok()) + .unwrap_or(u32::MAX); + let priority_rules = gateway_item + .children() + .find(|n| n.has_tag_name("priority-rule")) + .map(|n| { + n.children() + .filter_map(|n| { + if !n.has_tag_name("entry") { + return None; + } + let name = n.attribute("name").unwrap_or("").to_string(); + let priority: u32 = n + .children() + .find(|n| n.has_tag_name("priority")) + .and_then(|n| n.text()) + .and_then(|s| s.parse().ok()) + .unwrap_or(u32::MAX); + + Some(PriorityRule { name, priority }) + }) + .collect() + }) + .unwrap_or_default(); + + Some(Gateway { + name, + address, + priority, + priority_rules, + }) + }) + .collect(); + + Some(gateways) +} diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs new file mode 100644 index 0000000..770ec4b --- /dev/null +++ b/crates/gpapi/src/gp_params.rs @@ -0,0 +1,153 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::GP_USER_AGENT; + +#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)] +pub enum ClientOs { + Linux, + #[default] + Windows, + Mac, +} + +impl From<&ClientOs> for &str { + fn from(os: &ClientOs) -> Self { + match os { + ClientOs::Linux => "Linux", + ClientOs::Windows => "Windows", + ClientOs::Mac => "Mac", + } + } +} + +impl ClientOs { + pub fn to_openconnect_os(&self) -> &str { + match self { + ClientOs::Linux => "linux", + ClientOs::Windows => "win", + ClientOs::Mac => "mac-intel", + } + } +} + +#[derive(Debug, Serialize, Deserialize, Type, Default)] +pub struct GpParams { + user_agent: String, + client_os: ClientOs, + os_version: Option, + client_version: Option, + computer: Option, +} + +impl GpParams { + pub fn builder() -> GpParamsBuilder { + GpParamsBuilder::new() + } + + pub(crate) fn user_agent(&self) -> &str { + &self.user_agent + } + + pub(crate) fn computer(&self) -> &str { + match self.computer { + Some(ref computer) => computer, + None => (&self.client_os).into() + } + } + + pub(crate) fn to_params(&self) -> HashMap<&str, &str> { + let mut params: HashMap<&str, &str> = HashMap::new(); + let client_os: &str = (&self.client_os).into(); + + // Common params + params.insert("prot", "https:"); + params.insert("jnlpReady", "jnlpReady"); + params.insert("ok", "Login"); + params.insert("direct", "yes"); + params.insert("ipv6-support", "yes"); + params.insert("inputStr", ""); + params.insert("clientVer", "4100"); + + params.insert("clientos", client_os); + + if let Some(computer) = &self.computer { + params.insert("computer", computer); + } else { + params.insert("computer", client_os); + } + + if let Some(os_version) = &self.os_version { + params.insert("os-version", os_version); + } + + if let Some(client_version) = &self.client_version { + params.insert("clientgpversion", client_version); + } + + params + } +} + +pub struct GpParamsBuilder { + user_agent: String, + client_os: ClientOs, + os_version: Option, + client_version: Option, + computer: Option, +} + +impl GpParamsBuilder { + pub fn new() -> Self { + Self { + user_agent: GP_USER_AGENT.to_string(), + client_os: ClientOs::Linux, + os_version: Default::default(), + client_version: Default::default(), + computer: Default::default(), + } + } + + pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { + self.user_agent = user_agent.to_string(); + self + } + + pub fn client_os(&mut self, client_os: ClientOs) -> &mut Self { + self.client_os = client_os; + self + } + + pub fn os_version(&mut self, os_version: &str) -> &mut Self { + self.os_version = Some(os_version.to_string()); + self + } + + pub fn client_version(&mut self, client_version: &str) -> &mut Self { + self.client_version = Some(client_version.to_string()); + self + } + + pub fn computer(&mut self, computer: &str) -> &mut Self { + self.computer = Some(computer.to_string()); + self + } + + pub fn build(&self) -> GpParams { + GpParams { + user_agent: self.user_agent.clone(), + client_os: self.client_os.clone(), + os_version: self.os_version.clone(), + client_version: self.client_version.clone(), + computer: self.computer.clone(), + } + } +} + +impl Default for GpParamsBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/gpapi/src/lib.rs b/crates/gpapi/src/lib.rs new file mode 100644 index 0000000..ea4e001 --- /dev/null +++ b/crates/gpapi/src/lib.rs @@ -0,0 +1,28 @@ +pub mod auth; +pub mod credential; +pub mod gateway; +pub mod gp_params; +pub mod portal; +pub mod process; +pub mod service; +pub mod utils; + +#[cfg(debug_assertions)] +pub const GP_API_KEY: &[u8; 32] = &[0; 32]; + +pub const GP_USER_AGENT: &str = "PAN GlobalProtect"; +pub const GP_SERVICE_LOCK_FILE: &str = "/var/run/gpservice.lock"; + +#[cfg(not(debug_assertions))] +pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice"; +#[cfg(not(debug_assertions))] +pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui"; +#[cfg(not(debug_assertions))] +pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth"; + +#[cfg(debug_assertions)] +pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY"); +#[cfg(debug_assertions)] +pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY"); +#[cfg(debug_assertions)] +pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY"); diff --git a/crates/gpapi/src/portal/config.rs b/crates/gpapi/src/portal/config.rs new file mode 100644 index 0000000..4becfbb --- /dev/null +++ b/crates/gpapi/src/portal/config.rs @@ -0,0 +1,180 @@ +use anyhow::ensure; +use log::info; +use reqwest::Client; +use roxmltree::Document; +use serde::Serialize; +use specta::Type; +use thiserror::Error; + +use crate::{ + credential::{AuthCookieCredential, Credential}, + gateway::{parse_gateways, Gateway}, + gp_params::GpParams, + utils::{normalize_server, xml}, +}; + +#[derive(Debug, Serialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PortalConfig { + portal: String, + auth_cookie: AuthCookieCredential, + gateways: Vec, + config_digest: Option, +} + +impl PortalConfig { + pub fn new( + portal: String, + auth_cookie: AuthCookieCredential, + gateways: Vec, + config_digest: Option, + ) -> Self { + Self { + portal, + auth_cookie, + gateways, + config_digest, + } + } + + pub fn portal(&self) -> &str { + &self.portal + } + + pub fn gateways(&self) -> Vec<&Gateway> { + self.gateways.iter().collect() + } + + pub fn auth_cookie(&self) -> &AuthCookieCredential { + &self.auth_cookie + } + + /// In-place sort the gateways by region + pub fn sort_gateways(&mut self, region: &str) { + let preferred_gateway = self.find_preferred_gateway(region); + let preferred_gateway_index = self + .gateways() + .iter() + .position(|gateway| gateway.name == preferred_gateway.name) + .unwrap(); + + // Move the preferred gateway to the front of the list + self.gateways.swap(0, preferred_gateway_index); + } + + /// Find a gateway by name or address + pub fn find_gateway(&self, name_or_address: &str) -> Option<&Gateway> { + self + .gateways + .iter() + .find(|gateway| gateway.name == name_or_address || gateway.address == name_or_address) + } + + /// Find the preferred gateway for the given region + /// Iterates over the gateways and find the first one that + /// has the lowest priority for the given region. + /// If no gateway is found, returns the gateway with the lowest priority. + pub fn find_preferred_gateway(&self, region: &str) -> &Gateway { + let mut preferred_gateway: Option<&Gateway> = None; + let mut lowest_region_priority = u32::MAX; + + for gateway in &self.gateways { + for rule in &gateway.priority_rules { + if (rule.name == region || rule.name == "Any") && rule.priority < lowest_region_priority { + preferred_gateway = Some(gateway); + lowest_region_priority = rule.priority; + } + } + } + + // If no gateway is found, return the gateway with the lowest priority + preferred_gateway.unwrap_or_else(|| { + self + .gateways + .iter() + .min_by_key(|gateway| gateway.priority) + .unwrap() + }) + } +} + +#[derive(Error, Debug)] +pub enum PortalConfigError { + #[error("Empty response, retrying can help")] + EmptyResponse, + #[error("Empty auth cookie, retrying can help")] + EmptyAuthCookie, + #[error("Invalid auth cookie, retrying can help")] + InvalidAuthCookie, + #[error("Empty gateways, retrying can help")] + EmptyGateways, +} + +pub async fn retrieve_config( + portal: &str, + cred: &Credential, + gp_params: &GpParams, +) -> anyhow::Result { + let portal = normalize_server(portal)?; + let server = remove_url_scheme(&portal); + + let url = format!("{}/global-protect/getconfig.esp", portal); + let client = Client::builder() + .user_agent(gp_params.user_agent()) + .build()?; + + let mut params = cred.to_params(); + let extra_params = gp_params.to_params(); + + params.extend(extra_params); + params.insert("server", &server); + params.insert("host", &server); + + info!("Portal config, user_agent: {}", gp_params.user_agent()); + + let res_xml = client + .post(&url) + .form(¶ms) + .send() + .await? + .error_for_status()? + .text() + .await?; + + ensure!(!res_xml.is_empty(), PortalConfigError::EmptyResponse); + + let doc = Document::parse(&res_xml)?; + let gateways = parse_gateways(&doc).ok_or_else(|| anyhow::anyhow!("Failed to parse gateways"))?; + + let user_auth_cookie = xml::get_child_text(&doc, "portal-userauthcookie").unwrap_or_default(); + let prelogon_user_auth_cookie = + xml::get_child_text(&doc, "portal-prelogonuserauthcookie").unwrap_or_default(); + let config_digest = xml::get_child_text(&doc, "config-digest"); + + ensure!( + !user_auth_cookie.is_empty() && !prelogon_user_auth_cookie.is_empty(), + PortalConfigError::EmptyAuthCookie + ); + + ensure!( + user_auth_cookie != "empty" && prelogon_user_auth_cookie != "empty", + PortalConfigError::InvalidAuthCookie + ); + + ensure!(!gateways.is_empty(), PortalConfigError::EmptyGateways); + + Ok(PortalConfig::new( + server.to_string(), + AuthCookieCredential::new( + cred.username(), + &user_auth_cookie, + &prelogon_user_auth_cookie, + ), + gateways, + config_digest, + )) +} + +fn remove_url_scheme(s: &str) -> String { + s.replace("http://", "").replace("https://", "") +} diff --git a/crates/gpapi/src/portal/mod.rs b/crates/gpapi/src/portal/mod.rs new file mode 100644 index 0000000..8c111db --- /dev/null +++ b/crates/gpapi/src/portal/mod.rs @@ -0,0 +1,5 @@ +mod config; +mod prelogin; + +pub use config::*; +pub use prelogin::*; diff --git a/crates/gpapi/src/portal/prelogin.rs b/crates/gpapi/src/portal/prelogin.rs new file mode 100644 index 0000000..dbc4cab --- /dev/null +++ b/crates/gpapi/src/portal/prelogin.rs @@ -0,0 +1,129 @@ +use anyhow::bail; +use log::{info, trace}; +use reqwest::Client; +use roxmltree::Document; +use serde::Serialize; +use specta::Type; + +use crate::utils::{base64, normalize_server, xml}; + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SamlPrelogin { + region: String, + saml_request: String, +} + +impl SamlPrelogin { + pub fn region(&self) -> &str { + &self.region + } + + pub fn saml_request(&self) -> &str { + &self.saml_request + } +} + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StandardPrelogin { + region: String, + auth_message: String, + label_username: String, + label_password: String, +} + +impl StandardPrelogin { + pub fn region(&self) -> &str { + &self.region + } + + pub fn auth_message(&self) -> &str { + &self.auth_message + } + + pub fn label_username(&self) -> &str { + &self.label_username + } + + pub fn label_password(&self) -> &str { + &self.label_password + } +} + +#[derive(Debug, Serialize, Type, Clone)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Prelogin { + Saml(SamlPrelogin), + Standard(StandardPrelogin), +} + +impl Prelogin { + pub fn region(&self) -> &str { + match self { + Prelogin::Saml(saml) => saml.region(), + Prelogin::Standard(standard) => standard.region(), + } + } +} + +pub async fn prelogin(portal: &str, user_agent: &str) -> anyhow::Result { + info!("Portal prelogin, user_agent: {}", user_agent); + + let portal = normalize_server(portal)?; + let prelogin_url = format!("{}/global-protect/prelogin.esp", portal); + let client = Client::builder().user_agent(user_agent).build()?; + + let res_xml = client + .get(&prelogin_url) + .send() + .await? + .error_for_status()? + .text() + .await?; + + trace!("Prelogin response: {}", res_xml); + let doc = Document::parse(&res_xml)?; + + let status = xml::get_child_text(&doc, "status") + .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain status element"))?; + // Check the status of the prelogin response + if status.to_uppercase() != "SUCCESS" { + let msg = xml::get_child_text(&doc, "msg").unwrap_or(String::from("Unknown error")); + bail!("Prelogin failed: {}", msg) + } + + let region = xml::get_child_text(&doc, "region") + .ok_or_else(|| anyhow::anyhow!("Prelogin response does not contain region element"))?; + + let saml_method = xml::get_child_text(&doc, "saml-auth-method"); + let saml_request = xml::get_child_text(&doc, "saml-request"); + // Check if the prelogin response is SAML + if saml_method.is_some() && saml_request.is_some() { + let saml_request = base64::decode_to_string(&saml_request.unwrap())?; + let saml_prelogin = SamlPrelogin { + region, + saml_request, + }; + + return Ok(Prelogin::Saml(saml_prelogin)); + } + + let label_username = xml::get_child_text(&doc, "username-label"); + let label_password = xml::get_child_text(&doc, "password-label"); + // Check if the prelogin response is standard login + if label_username.is_some() && label_password.is_some() { + let auth_message = xml::get_child_text(&doc, "authentication-message") + .unwrap_or(String::from("Please enter the login credentials")); + let standard_prelogin = StandardPrelogin { + region, + auth_message, + label_username: label_username.unwrap(), + label_password: label_password.unwrap(), + }; + + return Ok(Prelogin::Standard(standard_prelogin)); + } + + bail!("Invalid prelogin response"); +} diff --git a/crates/gpapi/src/process/auth_launcher.rs b/crates/gpapi/src/process/auth_launcher.rs new file mode 100644 index 0000000..05389ed --- /dev/null +++ b/crates/gpapi/src/process/auth_launcher.rs @@ -0,0 +1,96 @@ +use std::process::Stdio; + +use tokio::process::Command; + +use crate::{auth::SamlAuthResult, credential::Credential, GP_AUTH_BINARY}; + +use super::command_traits::CommandExt; + +pub struct SamlAuthLauncher<'a> { + server: &'a str, + user_agent: Option<&'a str>, + saml_request: Option<&'a str>, + hidpi: bool, + fix_openssl: bool, + clean: bool, +} + +impl<'a> SamlAuthLauncher<'a> { + pub fn new(server: &'a str) -> Self { + Self { + server, + user_agent: None, + saml_request: None, + hidpi: false, + fix_openssl: false, + clean: false, + } + } + + pub fn user_agent(mut self, user_agent: &'a str) -> Self { + self.user_agent = Some(user_agent); + self + } + + pub fn saml_request(mut self, saml_request: &'a str) -> Self { + self.saml_request = Some(saml_request); + self + } + + pub fn hidpi(mut self, hidpi: bool) -> Self { + self.hidpi = hidpi; + self + } + + pub fn fix_openssl(mut self, fix_openssl: bool) -> Self { + self.fix_openssl = fix_openssl; + self + } + + pub fn clean(mut self, clean: bool) -> Self { + self.clean = clean; + self + } + + /// Launch the authenticator binary as the current user or SUDO_USER if available. + pub async fn launch(self) -> anyhow::Result { + let mut auth_cmd = Command::new(GP_AUTH_BINARY); + auth_cmd.arg(self.server); + + if let Some(user_agent) = self.user_agent { + auth_cmd.arg("--user-agent").arg(user_agent); + } + + if let Some(saml_request) = self.saml_request { + auth_cmd.arg("--saml-request").arg(saml_request); + } + + if self.fix_openssl { + auth_cmd.arg("--fix-openssl"); + } + + if self.hidpi { + auth_cmd.arg("--hidpi"); + } + + if self.clean { + auth_cmd.arg("--clean"); + } + + let mut non_root_cmd = auth_cmd.into_non_root()?; + let output = non_root_cmd + .kill_on_drop(true) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output() + .await?; + + let auth_result: SamlAuthResult = serde_json::from_slice(&output.stdout) + .map_err(|_| anyhow::anyhow!("Failed to parse auth data"))?; + + match auth_result { + SamlAuthResult::Success(auth_data) => Credential::try_from(auth_data), + SamlAuthResult::Failure(msg) => Err(anyhow::anyhow!(msg)), + } + } +} diff --git a/crates/gpapi/src/process/command_traits.rs b/crates/gpapi/src/process/command_traits.rs new file mode 100644 index 0000000..e4dba3d --- /dev/null +++ b/crates/gpapi/src/process/command_traits.rs @@ -0,0 +1,64 @@ +use anyhow::bail; +use std::{env, ffi::OsStr}; +use tokio::process::Command; +use users::{os::unix::UserExt, User}; + +pub trait CommandExt { + fn new_pkexec>(program: S) -> Command; + fn into_non_root(self) -> anyhow::Result; +} + +impl CommandExt for Command { + fn new_pkexec>(program: S) -> Command { + let mut cmd = Command::new("pkexec"); + cmd + .arg("--disable-internal-agent") + .arg("--user") + .arg("root") + .arg(program); + + cmd + } + + fn into_non_root(mut self) -> anyhow::Result { + let user = + get_non_root_user().map_err(|_| anyhow::anyhow!("{:?} cannot be run as root", self))?; + + self + .env("HOME", user.home_dir()) + .env("USER", user.name()) + .env("LOGNAME", user.name()) + .env("USERNAME", user.name()) + .uid(user.uid()) + .gid(user.primary_group_id()); + + Ok(self) + } +} + +fn get_non_root_user() -> anyhow::Result { + let current_user = whoami::username(); + + let user = if current_user == "root" { + get_real_user()? + } else { + users::get_user_by_name(¤t_user) + .ok_or_else(|| anyhow::anyhow!("User ({}) not found", current_user))? + }; + + if user.uid() == 0 { + bail!("Non-root user not found") + } + + Ok(user) +} + +fn get_real_user() -> anyhow::Result { + // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. + let uid = match env::var("SUDO_UID") { + Ok(uid) => uid.parse::()?, + _ => env::var("PKEXEC_UID")?.parse::()?, + }; + + users::get_user_by_uid(uid).ok_or_else(|| anyhow::anyhow!("User not found")) +} diff --git a/crates/gpapi/src/process/gui_launcher.rs b/crates/gpapi/src/process/gui_launcher.rs new file mode 100644 index 0000000..47dc6c2 --- /dev/null +++ b/crates/gpapi/src/process/gui_launcher.rs @@ -0,0 +1,91 @@ +use std::{ + collections::HashMap, + path::PathBuf, + process::{ExitStatus, Stdio}, +}; + +use tokio::{io::AsyncWriteExt, process::Command}; + +use crate::{utils::base64, GP_GUI_BINARY}; + +use super::command_traits::CommandExt; + +pub struct GuiLauncher { + program: PathBuf, + api_key: Option>, + minimized: bool, + envs: Option>, +} + +impl Default for GuiLauncher { + fn default() -> Self { + Self::new() + } +} + +impl GuiLauncher { + pub fn new() -> Self { + Self { + program: GP_GUI_BINARY.into(), + api_key: None, + minimized: false, + envs: None, + } + } + + pub fn envs>>>(mut self, envs: T) -> Self { + self.envs = envs.into(); + self + } + + pub fn api_key(mut self, api_key: Vec) -> Self { + self.api_key = Some(api_key); + self + } + + pub fn minimized(mut self, minimized: bool) -> Self { + self.minimized = minimized; + self + } + + pub async fn launch(&self) -> anyhow::Result { + let mut cmd = Command::new(&self.program); + + if let Some(envs) = &self.envs { + cmd.env_clear(); + cmd.envs(envs); + } + + if self.api_key.is_some() { + cmd.arg("--api-key-on-stdin"); + } + + if self.minimized { + cmd.arg("--minimized"); + } + + let mut non_root_cmd = cmd.into_non_root()?; + + let mut child = non_root_cmd + .kill_on_drop(true) + .stdin(Stdio::piped()) + .spawn()?; + + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?; + + if let Some(api_key) = &self.api_key { + let api_key = base64::encode(api_key); + tokio::spawn(async move { + stdin.write_all(api_key.as_bytes()).await.unwrap(); + drop(stdin); + }); + } + + let exit_status = child.wait().await?; + + Ok(exit_status) + } +} diff --git a/crates/gpapi/src/process/mod.rs b/crates/gpapi/src/process/mod.rs new file mode 100644 index 0000000..e82d429 --- /dev/null +++ b/crates/gpapi/src/process/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod command_traits; + +pub mod auth_launcher; +pub mod gui_launcher; +pub mod service_launcher; diff --git a/crates/gpapi/src/process/service_launcher.rs b/crates/gpapi/src/process/service_launcher.rs new file mode 100644 index 0000000..05bca37 --- /dev/null +++ b/crates/gpapi/src/process/service_launcher.rs @@ -0,0 +1,72 @@ +use std::{ + fs::File, + path::PathBuf, + process::{ExitStatus, Stdio}, +}; + +use tokio::process::Command; + +use crate::GP_SERVICE_BINARY; + +use super::command_traits::CommandExt; + +pub struct ServiceLauncher { + program: PathBuf, + minimized: bool, + env_file: Option, + log_file: Option, +} + +impl Default for ServiceLauncher { + fn default() -> Self { + Self::new() + } +} + +impl ServiceLauncher { + pub fn new() -> Self { + Self { + program: GP_SERVICE_BINARY.into(), + minimized: false, + env_file: None, + log_file: None, + } + } + + pub fn minimized(mut self, minimized: bool) -> Self { + self.minimized = minimized; + self + } + + pub fn env_file(mut self, env_file: &str) -> Self { + self.env_file = Some(env_file.to_string()); + self + } + + pub fn log_file(mut self, log_file: &str) -> Self { + self.log_file = Some(log_file.to_string()); + self + } + + pub async fn launch(&self) -> anyhow::Result { + let mut cmd = Command::new_pkexec(&self.program); + + if self.minimized { + cmd.arg("--minimized"); + } + + if let Some(env_file) = &self.env_file { + cmd.arg("--env-file").arg(env_file); + } + + if let Some(log_file) = &self.log_file { + let log_file = File::create(log_file)?; + let stdio = Stdio::from(log_file); + cmd.stderr(stdio); + } + + let exit_status = cmd.kill_on_drop(true).spawn()?.wait().await?; + + Ok(exit_status) + } +} diff --git a/crates/gpapi/src/service/event.rs b/crates/gpapi/src/service/event.rs new file mode 100644 index 0000000..869b980 --- /dev/null +++ b/crates/gpapi/src/service/event.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use super::vpn_state::VpnState; + +/// Events that can be emitted by the service +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum WsEvent { + VpnState(VpnState), + ActiveGui, +} diff --git a/crates/gpapi/src/service/mod.rs b/crates/gpapi/src/service/mod.rs new file mode 100644 index 0000000..7fd4b55 --- /dev/null +++ b/crates/gpapi/src/service/mod.rs @@ -0,0 +1,3 @@ +pub mod event; +pub mod request; +pub mod vpn_state; diff --git a/crates/gpapi/src/service/request.rs b/crates/gpapi/src/service/request.rs new file mode 100644 index 0000000..753b9ba --- /dev/null +++ b/crates/gpapi/src/service/request.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::{gateway::Gateway, gp_params::ClientOs}; + +use super::vpn_state::ConnectInfo; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LaunchGuiRequest { + user: String, + envs: HashMap, +} + +impl LaunchGuiRequest { + pub fn new(user: String, envs: HashMap) -> Self { + Self { user, envs } + } + + pub fn user(&self) -> &str { + &self.user + } + + pub fn envs(&self) -> &HashMap { + &self.envs + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct ConnectArgs { + cookie: String, + vpnc_script: Option, + user_agent: Option, + os: Option, +} + +impl ConnectArgs { + pub fn new(cookie: String) -> Self { + Self { + cookie, + vpnc_script: None, + user_agent: None, + os: None, + } + } + + pub fn cookie(&self) -> &str { + &self.cookie + } + + pub fn vpnc_script(&self) -> Option { + self.vpnc_script.clone() + } + + pub fn user_agent(&self) -> Option { + self.user_agent.clone() + } + + pub fn openconnect_os(&self) -> Option { + self + .os + .as_ref() + .map(|os| os.to_openconnect_os().to_string()) + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct ConnectRequest { + info: ConnectInfo, + args: ConnectArgs, +} + +impl ConnectRequest { + pub fn new(info: ConnectInfo, cookie: String) -> Self { + Self { + info, + args: ConnectArgs::new(cookie), + } + } + + pub fn with_vpnc_script>>(mut self, vpnc_script: T) -> Self { + self.args.vpnc_script = vpnc_script.into(); + self + } + + pub fn with_user_agent>>(mut self, user_agent: T) -> Self { + self.args.user_agent = user_agent.into(); + self + } + + pub fn with_os>>(mut self, os: T) -> Self { + self.args.os = os.into(); + self + } + + pub fn gateway(&self) -> &Gateway { + self.info.gateway() + } + + pub fn info(&self) -> &ConnectInfo { + &self.info + } + + pub fn args(&self) -> &ConnectArgs { + &self.args + } +} + +#[derive(Debug, Deserialize, Serialize, Type)] +pub struct DisconnectRequest; + +/// Requests that can be sent to the service +#[derive(Debug, Deserialize, Serialize)] +pub enum WsRequest { + Connect(Box), + Disconnect(DisconnectRequest), +} diff --git a/crates/gpapi/src/service/vpn_state.rs b/crates/gpapi/src/service/vpn_state.rs new file mode 100644 index 0000000..f800395 --- /dev/null +++ b/crates/gpapi/src/service/vpn_state.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +use crate::gateway::Gateway; + +#[derive(Debug, Deserialize, Serialize, Type, Clone)] +pub struct ConnectInfo { + portal: String, + gateway: Gateway, + gateways: Vec, +} + +impl ConnectInfo { + pub fn new(portal: String, gateway: Gateway, gateways: Vec) -> Self { + Self { + portal, + gateway, + gateways, + } + } + + pub fn gateway(&self) -> &Gateway { + &self.gateway + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum VpnState { + Disconnected, + Connecting(Box), + Connected(Box), + Disconnecting, +} diff --git a/crates/gpapi/src/utils/base64.rs b/crates/gpapi/src/utils/base64.rs new file mode 100644 index 0000000..d3bc064 --- /dev/null +++ b/crates/gpapi/src/utils/base64.rs @@ -0,0 +1,21 @@ +use base64::{engine::general_purpose, Engine}; + +pub fn encode(data: &[u8]) -> String { + let engine = general_purpose::STANDARD; + + engine.encode(data) +} + +pub fn decode_to_vec(s: &str) -> anyhow::Result> { + let engine = general_purpose::STANDARD; + let decoded = engine.decode(s)?; + + Ok(decoded) +} + +pub(crate) fn decode_to_string(s: &str) -> anyhow::Result { + let decoded = decode_to_vec(s)?; + let decoded = String::from_utf8(decoded)?; + + Ok(decoded) +} diff --git a/crates/gpapi/src/utils/crypto.rs b/crates/gpapi/src/utils/crypto.rs new file mode 100644 index 0000000..7fb9d73 --- /dev/null +++ b/crates/gpapi/src/utils/crypto.rs @@ -0,0 +1,108 @@ +use chacha20poly1305::{ + aead::{Aead, OsRng}, + AeadCore, ChaCha20Poly1305, Key, KeyInit, Nonce, +}; +use serde::{de::DeserializeOwned, Serialize}; + +pub fn generate_key() -> Key { + ChaCha20Poly1305::generate_key(&mut OsRng) +} + +pub fn encrypt(key: &Key, value: &T) -> anyhow::Result> +where + T: Serialize, +{ + let cipher = ChaCha20Poly1305::new(key); + let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); + + let data = serde_json::to_vec(value)?; + let cipher_text = cipher.encrypt(&nonce, data.as_ref())?; + + let mut encrypted = Vec::new(); + encrypted.extend_from_slice(&nonce); + encrypted.extend_from_slice(&cipher_text); + + Ok(encrypted) +} + +pub fn decrypt(key: &Key, encrypted: Vec) -> anyhow::Result +where + T: DeserializeOwned, +{ + let cipher = ChaCha20Poly1305::new(key); + + let nonce = Nonce::from_slice(&encrypted[..12]); + let cipher_text = &encrypted[12..]; + + let plaintext = cipher.decrypt(nonce, cipher_text)?; + + let value = serde_json::from_slice(&plaintext)?; + + Ok(value) +} + +pub struct Crypto { + key: Vec, +} + +impl Crypto { + pub fn new(key: Vec) -> Self { + Self { key } + } + + pub fn encrypt(&self, plain: T) -> anyhow::Result> { + let key: &[u8] = &self.key; + let encrypted_data = encrypt(key.into(), &plain)?; + + Ok(encrypted_data) + } + + pub fn decrypt(&self, encrypted: Vec) -> anyhow::Result { + let key: &[u8] = &self.key; + decrypt(key.into(), encrypted) + } + + pub fn encrypt_to(&self, path: &std::path::Path, plain: T) -> anyhow::Result<()> { + let encrypted_data = self.encrypt(plain)?; + std::fs::write(path, encrypted_data)?; + + Ok(()) + } + + pub fn decrypt_from(&self, path: &std::path::Path) -> anyhow::Result { + let encrypted_data = std::fs::read(path)?; + self.decrypt(encrypted_data) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + + #[derive(Serialize, Deserialize)] + struct User { + name: String, + age: u8, + } + + #[test] + fn it_works() -> anyhow::Result<()> { + let key = generate_key(); + + let user = User { + name: "test".to_string(), + age: 18, + }; + + let encrypted = encrypt(&key, &user)?; + + let decrypted_user = decrypt::(&key, encrypted)?; + + assert_eq!(user.name, decrypted_user.name); + assert_eq!(user.age, decrypted_user.age); + + Ok(()) + } +} diff --git a/crates/gpapi/src/utils/endpoint.rs b/crates/gpapi/src/utils/endpoint.rs new file mode 100644 index 0000000..0a88bc6 --- /dev/null +++ b/crates/gpapi/src/utils/endpoint.rs @@ -0,0 +1,20 @@ +use tokio::fs; + +use crate::GP_SERVICE_LOCK_FILE; + +async fn read_port() -> anyhow::Result { + let port = fs::read_to_string(GP_SERVICE_LOCK_FILE).await?; + Ok(port.trim().to_string()) +} + +pub async fn http_endpoint() -> anyhow::Result { + let port = read_port().await?; + + Ok(format!("http://127.0.0.1:{}", port)) +} + +pub async fn ws_endpoint() -> anyhow::Result { + let port = read_port().await?; + + Ok(format!("ws://127.0.0.1:{}/ws", port)) +} diff --git a/crates/gpapi/src/utils/env_file.rs b/crates/gpapi/src/utils/env_file.rs new file mode 100644 index 0000000..76d52c3 --- /dev/null +++ b/crates/gpapi/src/utils/env_file.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; +use std::env; +use std::io::Write; +use std::path::Path; + +use tempfile::NamedTempFile; + +pub fn persist_env_vars(extra: Option>) -> anyhow::Result { + let mut env_file = NamedTempFile::new()?; + let content = env::vars() + .map(|(key, value)| format!("{}={}", key, value)) + .chain( + extra + .unwrap_or_default() + .into_iter() + .map(|(key, value)| format!("{}={}", key, value)), + ) + .collect::>() + .join("\n"); + + writeln!(env_file, "{}", content)?; + + Ok(env_file) +} + +pub fn load_env_vars>(env_file: T) -> anyhow::Result> { + let content = std::fs::read_to_string(env_file)?; + let mut env_vars: HashMap = HashMap::new(); + + for line in content.lines() { + if let Some((key, value)) = line.split_once('=') { + env_vars.insert(key.to_string(), value.to_string()); + } + } + + Ok(env_vars) +} diff --git a/crates/gpapi/src/utils/lock_file.rs b/crates/gpapi/src/utils/lock_file.rs new file mode 100644 index 0000000..93ee510 --- /dev/null +++ b/crates/gpapi/src/utils/lock_file.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +pub struct LockFile { + path: PathBuf, +} + +impl LockFile { + pub fn new>(path: P) -> Self { + Self { path: path.into() } + } + + pub fn exists(&self) -> bool { + self.path.exists() + } + + pub fn lock(&self, content: impl AsRef<[u8]>) -> anyhow::Result<()> { + std::fs::write(&self.path, content)?; + Ok(()) + } + + pub fn unlock(&self) -> anyhow::Result<()> { + std::fs::remove_file(&self.path)?; + Ok(()) + } + + pub async fn check_health(&self) -> bool { + match std::fs::read_to_string(&self.path) { + Ok(content) => { + let url = format!("http://127.0.0.1:{}/health", content.trim()); + + match reqwest::get(&url).await { + Ok(resp) => resp.status().is_success(), + Err(_) => false, + } + } + Err(_) => false, + } + } +} diff --git a/crates/gpapi/src/utils/mod.rs b/crates/gpapi/src/utils/mod.rs new file mode 100644 index 0000000..048c23c --- /dev/null +++ b/crates/gpapi/src/utils/mod.rs @@ -0,0 +1,40 @@ +use reqwest::Url; + +pub(crate) mod xml; + +pub mod base64; +pub mod crypto; +pub mod endpoint; +pub mod env_file; +pub mod lock_file; +pub mod openssl; +pub mod redact; +#[cfg(feature = "tauri")] +pub mod window; + +mod shutdown_signal; + +pub use shutdown_signal::shutdown_signal; + +/// Normalize the server URL to the format `https://:` +pub fn normalize_server(server: &str) -> anyhow::Result { + let server = if server.starts_with("https://") || server.starts_with("http://") { + server.to_string() + } else { + format!("https://{}", server) + }; + + let normalized_url = Url::parse(&server)?; + let scheme = normalized_url.scheme(); + let host = normalized_url + .host_str() + .ok_or(anyhow::anyhow!("Invalid server URL: missing host"))?; + + let port: String = normalized_url + .port() + .map_or("".into(), |port| format!(":{}", port)); + + let normalized_url = format!("{}://{}{}", scheme, host, port); + + Ok(normalized_url) +} diff --git a/crates/gpapi/src/utils/openssl.rs b/crates/gpapi/src/utils/openssl.rs new file mode 100644 index 0000000..d2aea55 --- /dev/null +++ b/crates/gpapi/src/utils/openssl.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use tempfile::NamedTempFile; + +pub fn openssl_conf() -> String { + let option = "UnsafeLegacyServerConnect"; + + format!( + "openssl_conf = openssl_init + +[openssl_init] +ssl_conf = ssl_sect + +[ssl_sect] +system_default = system_default_sect + +[system_default_sect] +Options = {}", + option + ) +} + +pub fn fix_openssl>(path: P) -> anyhow::Result<()> { + let content = openssl_conf(); + std::fs::write(path, content)?; + Ok(()) +} + +pub fn fix_openssl_env() -> anyhow::Result { + let openssl_conf = NamedTempFile::new()?; + let openssl_conf_path = openssl_conf.path(); + + fix_openssl(openssl_conf_path)?; + std::env::set_var("OPENSSL_CONF", openssl_conf_path); + + Ok(openssl_conf) +} diff --git a/crates/gpapi/src/utils/redact.rs b/crates/gpapi/src/utils/redact.rs new file mode 100644 index 0000000..7917b18 --- /dev/null +++ b/crates/gpapi/src/utils/redact.rs @@ -0,0 +1,227 @@ +use std::sync::RwLock; + +use redact_engine::{Pattern, Redaction as RedactEngine}; +use regex::Regex; +use url::{form_urlencoded, Url}; + +pub struct Redaction { + redact_engine: RwLock>, +} + +impl Default for Redaction { + fn default() -> Self { + Self::new() + } +} + +impl Redaction { + pub fn new() -> Self { + let redact_engine = RedactEngine::custom("[**********]").add_pattern(Pattern { + test: Regex::new("(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})").unwrap(), + group: 1, + }); + + Self { + redact_engine: RwLock::new(Some(redact_engine)), + } + } + + pub fn add_value(&self, text: &str) -> anyhow::Result<()> { + let mut redact_engine = self + .redact_engine + .write() + .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; + + *redact_engine = Some( + redact_engine + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? + .add_value(text)?, + ); + + Ok(()) + } + + pub fn add_values(&self, texts: &[&str]) -> anyhow::Result<()> { + let mut redact_engine = self + .redact_engine + .write() + .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on redact engine"))?; + + *redact_engine = Some( + redact_engine + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to take redact engine"))? + .add_values(texts.to_vec())?, + ); + + Ok(()) + } + + pub fn redact_str(&self, text: &str) -> String { + self + .redact_engine + .read() + .expect("Failed to acquire read lock on redact engine") + .as_ref() + .expect("Failed to get redact engine") + .redact_str(text) + } +} + +/// Redact a value by replacing all but the first and last character with asterisks, +/// The length of the value to be redacted must be at least 3 characters. +/// e.g. "foo" -> "f**********o" +pub fn redact_value(text: &str) -> String { + if text.len() < 3 { + return text.to_string(); + } + + let mut redacted = String::new(); + redacted.push_str(&text[0..1]); + redacted.push_str(&"*".repeat(10)); + redacted.push_str(&text[text.len() - 1..]); + + redacted +} + +pub fn redact_uri(uri: &str) -> String { + let Ok(mut url) = Url::parse(uri) else { + return uri.to_string(); + }; + + // Could be a data: URI + if url.cannot_be_a_base() { + if url.scheme() == "about" { + return uri.to_string(); + } + + if url.path().len() > 15 { + return format!( + "{}:{}{}", + url.scheme(), + &url.path()[0..10], + redact_value(&url.path()[10..]) + ); + } + + return format!("{}:{}", url.scheme(), redact_value(url.path())); + } + + let host = url.host_str().unwrap_or_default(); + if url.set_host(Some(&redact_value(host))).is_err() { + let redacted_query = redact_query(url.query()) + .as_deref() + .map(|query| format!("?{}", query)) + .unwrap_or_default(); + + return format!( + "{}://[**********]{}{}", + url.scheme(), + url.path(), + redacted_query + ); + } + + let redacted_query = redact_query(url.query()); + url.set_query(redacted_query.as_deref()); + url.to_string() +} + +fn redact_query(query: Option<&str>) -> Option { + let query = query?; + + let query_pairs = form_urlencoded::parse(query.as_bytes()); + let mut redacted_pairs = query_pairs.map(|(key, value)| (key, redact_value(&value))); + + let query = form_urlencoded::Serializer::new(String::new()) + .extend_pairs(redacted_pairs.by_ref()) + .finish(); + + Some(query) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_should_not_redact_value() { + let text = "fo"; + + assert_eq!(redact_value(text), "fo"); + } + + #[test] + fn it_should_redact_value() { + let text = "foo"; + + assert_eq!(redact_value(text), "f**********o"); + } + + #[test] + fn it_should_redact_dynamic_value() { + let redaction = Redaction::new(); + + redaction.add_value("foo").unwrap(); + + assert_eq!( + redaction.redact_str("hello, foo, bar"), + "hello, [**********], bar" + ); + } + + #[test] + fn it_should_redact_dynamic_values() { + let redaction = Redaction::new(); + + redaction.add_values(&["foo", "bar"]).unwrap(); + + assert_eq!( + redaction.redact_str("hello, foo, bar"), + "hello, [**********], [**********]" + ); + } + + #[test] + fn it_should_redact_uri() { + let uri = "https://foo.bar"; + assert_eq!(redact_uri(uri), "https://f**********r/"); + + let uri = "https://foo.bar/"; + assert_eq!(redact_uri(uri), "https://f**********r/"); + + let uri = "https://foo.bar/baz"; + assert_eq!(redact_uri(uri), "https://f**********r/baz"); + + let uri = "https://foo.bar/baz?qux=quux"; + assert_eq!(redact_uri(uri), "https://f**********r/baz?qux=q**********x"); + } + + #[test] + fn it_should_redact_data_uri() { + let uri = "data:text/plain;a"; + assert_eq!(redact_uri(uri), "data:t**********a"); + + let uri = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="; + assert_eq!(redact_uri(uri), "data:text/plain;**********="); + + let uri = "about:blank"; + assert_eq!(redact_uri(uri), "about:blank"); + } + + #[test] + fn it_should_redact_ipv6() { + let uri = "https://[2001:db8::1]:8080"; + assert_eq!(redact_uri(uri), "https://[**********]/"); + + let uri = "https://[2001:db8::1]:8080/"; + assert_eq!(redact_uri(uri), "https://[**********]/"); + + let uri = "https://[2001:db8::1]:8080/baz"; + assert_eq!(redact_uri(uri), "https://[**********]/baz"); + + let uri = "https://[2001:db8::1]:8080/baz?qux=quux"; + assert_eq!(redact_uri(uri), "https://[**********]/baz?qux=q**********x"); + } +} diff --git a/crates/gpapi/src/utils/shutdown_signal.rs b/crates/gpapi/src/utils/shutdown_signal.rs new file mode 100644 index 0000000..bab9069 --- /dev/null +++ b/crates/gpapi/src/utils/shutdown_signal.rs @@ -0,0 +1,22 @@ +use tokio::signal; + +pub async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} diff --git a/crates/gpapi/src/utils/window.rs b/crates/gpapi/src/utils/window.rs new file mode 100644 index 0000000..1b10d41 --- /dev/null +++ b/crates/gpapi/src/utils/window.rs @@ -0,0 +1,90 @@ +use std::{process::ExitStatus, time::Duration}; + +use anyhow::bail; +use log::{info, warn}; +use tauri::{window::MenuHandle, Window}; +use tokio::process::Command; + +pub trait WindowExt { + fn raise(&self) -> anyhow::Result<()>; +} + +impl WindowExt for Window { + fn raise(&self) -> anyhow::Result<()> { + raise_window(self) + } +} + +pub fn raise_window(win: &Window) -> anyhow::Result<()> { + let is_wayland = std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland"; + + if is_wayland { + win.hide()?; + win.show()?; + } else { + if !win.is_visible()? { + win.show()?; + } + let title = win.title()?; + tokio::spawn(async move { + info!("Raising window: {}", title); + if let Err(err) = wmctrl_raise_window(&title).await { + warn!("Failed to raise window: {}", err); + } + }); + } + + // Calling window.show() on Windows will cause the menu to be shown. + hide_menu(win.menu_handle()); + + Ok(()) +} + +async fn wmctrl_raise_window(title: &str) -> anyhow::Result<()> { + let mut counter = 0; + + loop { + if let Ok(exit_status) = wmctrl_try_raise_window(title).await { + if exit_status.success() { + info!("Window raised after {} attempts", counter + 1); + return Ok(()); + } + } + + if counter >= 10 { + bail!("Failed to raise window: {}", title) + } + + counter += 1; + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result { + let exit_status = Command::new("wmctrl") + .arg("-F") + .arg("-a") + .arg(title) + .spawn()? + .wait() + .await?; + + Ok(exit_status) +} + +fn hide_menu(menu_handle: MenuHandle) { + tokio::spawn(async move { + loop { + let menu_visible = menu_handle.is_visible().unwrap_or(false); + + if !menu_visible { + break; + } + + if menu_visible { + let _ = menu_handle.hide(); + tokio::time::sleep(Duration::from_millis(10)).await; + } + } + }); +} diff --git a/crates/gpapi/src/utils/xml.rs b/crates/gpapi/src/utils/xml.rs new file mode 100644 index 0000000..674e866 --- /dev/null +++ b/crates/gpapi/src/utils/xml.rs @@ -0,0 +1,6 @@ +use roxmltree::Document; + +pub(crate) fn get_child_text(doc: &Document, name: &str) -> Option { + let node = doc.descendants().find(|n| n.has_tag_name(name))?; + node.text().map(|s| s.to_string()) +} diff --git a/crates/gpapi/tests/files/gateway_login.xml b/crates/gpapi/tests/files/gateway_login.xml new file mode 100644 index 0000000..cfb981f --- /dev/null +++ b/crates/gpapi/tests/files/gateway_login.xml @@ -0,0 +1,27 @@ + + + + (null) + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + XXX-GP-Gateway-N + user + AD_Authentication + vsys1 + corp.example.com + (null) + + + + tunnel + -1 + 4100 + + xxxxxx + aaaaaa + + 4 + unknown + + + \ No newline at end of file diff --git a/crates/gpapi/tests/files/portal_config.xml b/crates/gpapi/tests/files/portal_config.xml new file mode 100644 index 0000000..0aab3ca --- /dev/null +++ b/crates/gpapi/tests/files/portal_config.xml @@ -0,0 +1,212 @@ + + + vpn.example.com + 4100 + 6.0.1-19 + global-protect-full + **** + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + yes + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + yes + + + + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + + no + + + on-demand + yes + yes + 24 + + + + + yes + yes + + 365 + + vpn.example.com + + yes + + + + 5 + + + + + + 1 + + + 1 + vpn_gateway + + + + + + 5 + + + + xxx.xxx.xxx.xxx + + + 1 + + + 1 + + + + + + yes + + + 0 + 0 + + + + no + + + allowed + yes + yes + yes + yes + + no + no + + + 3600 + 20 + yes + + + antivirus + anti-spyware + host-info + data-loss-prevention + patch-management + firewall + anti-malware + disk-backup + disk-encryption + + + + + 1 + no + no + no + no + + allowed + prompt + yes + no + no + yes + yes + no + 30 + 5 + no + no + + + 0 + + 15 + yes + <div style="font-family:'Helvetica + Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: + 30px;">Notice</h1><p style="margin: 0;font-size: 15px; + line-height: 1.2em;">To access the network, you must first connect to + GlobalProtect.</p></div> + yes + no + <div style="font-family:'Helvetica + Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size: + 30px;">Captive Portal Detected</h1><p style="margin: 0; font-size: + 15px; line-height: 1.2em;">GlobalProtect has temporarily permitted network + access for you to connect to the Internet. Follow instructions from your internet + provider.</p><p style="margin: 0; font-size: 15px; line-height: + 1.2em;">If you let the connection time out, open GlobalProtect and click Connect + to try again.</p></div> + 5 + user-and-machine + 7 + + yes + no + yes + yes + yes + 0 + 0 + 0 + 0 + yes + 0 + 1400 + 0 + no + 30 + 60 + 30 + network-traffic + yes + no + no + + no + yes + yes + no + 4501 + + You have attempted to access a protected resource that requires + additional authentication. Proceed to authenticate at + 0 + yes + + no + no + yes + + not-install + Access to the network from this device has been restricted as per + your organization's security policy. Please contact your IT Administrator. + Access to the network from this device has been restored as per + your organization's security policy. + + + user@example.com + xxxxxx + xxxxxx + 2d8e997765a2f59cbf80284b2f2fbd38 + diff --git a/crates/gpapi/tests/files/prelogin_saml.xml b/crates/gpapi/tests/files/prelogin_saml.xml new file mode 100644 index 0000000..bc07d9d --- /dev/null +++ b/crates/gpapi/tests/files/prelogin_saml.xml @@ -0,0 +1,22 @@ + + + Success + + false + + + Enter login credentials + Username + Password + 1 + yes + + + 0 + REDIRECT + 600 + 0 + U0FNTFJlcXVlc3Q9eHh4 + no + CN + \ No newline at end of file diff --git a/crates/gpapi/tests/files/prelogin_standard.xml b/crates/gpapi/tests/files/prelogin_standard.xml new file mode 100644 index 0000000..9d2ebab --- /dev/null +++ b/crates/gpapi/tests/files/prelogin_standard.xml @@ -0,0 +1,15 @@ + + + Success + + false + + + Enter login credentials + Username + Password + 1 + yes + no + US + \ No newline at end of file diff --git a/crates/openconnect/Cargo.toml b/crates/openconnect/Cargo.toml new file mode 100644 index 0000000..d3216a9 --- /dev/null +++ b/crates/openconnect/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "openconnect" +version.workspace = true +edition.workspace = true +license.workspace = true +links = "openconnect" + +[dependencies] +log.workspace = true +is_executable.workspace = true + +[build-dependencies] +cc = "1" diff --git a/crates/openconnect/build.rs b/crates/openconnect/build.rs new file mode 100644 index 0000000..42eb240 --- /dev/null +++ b/crates/openconnect/build.rs @@ -0,0 +1,12 @@ +fn main() { + // Link to the native openconnect library + println!("cargo:rustc-link-lib=openconnect"); + println!("cargo:rerun-if-changed=src/ffi/vpn.c"); + println!("cargo:rerun-if-changed=src/ffi/vpn.h"); + + // Compile the vpn.c file + cc::Build::new() + .file("src/ffi/vpn.c") + .include("src/ffi") + .compile("vpn"); +} diff --git a/crates/openconnect/src/ffi/mod.rs b/crates/openconnect/src/ffi/mod.rs new file mode 100644 index 0000000..733f246 --- /dev/null +++ b/crates/openconnect/src/ffi/mod.rs @@ -0,0 +1,71 @@ +use crate::Vpn; +use log::{debug, info, trace, warn}; +use std::ffi::{c_char, c_int, c_void}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct ConnectOptions { + pub user_data: *mut c_void, + + pub server: *const c_char, + pub cookie: *const c_char, + pub user_agent: *const c_char, + + pub script: *const c_char, + pub os: *const c_char, + pub certificate: *const c_char, + pub servercert: *const c_char, +} + +#[link(name = "vpn")] +extern "C" { + #[link_name = "vpn_connect"] + fn vpn_connect( + options: *const ConnectOptions, + callback: extern "C" fn(i32, *mut c_void), + ) -> c_int; + + #[link_name = "vpn_disconnect"] + fn vpn_disconnect(); +} + +pub(crate) fn connect(options: &ConnectOptions) -> i32 { + unsafe { vpn_connect(options, on_vpn_connected) } +} + +pub(crate) fn disconnect() { + unsafe { vpn_disconnect() } +} + +#[no_mangle] +extern "C" fn on_vpn_connected(pipe_fd: i32, vpn: *mut c_void) { + let vpn = unsafe { &*(vpn as *const Vpn) }; + vpn.on_connected(pipe_fd); +} + +// Logger used in the C code. +// level: 0 = error, 1 = info, 2 = debug, 3 = trace +// map the error level log in openconnect to the warning level +#[no_mangle] +extern "C" fn vpn_log(level: i32, message: *const c_char) { + let message = unsafe { std::ffi::CStr::from_ptr(message) }; + let message = message.to_str().unwrap_or("Invalid log message"); + // Strip the trailing newline + let message = message.trim_end_matches('\n'); + + if level == 0 { + warn!("{}", message); + } else if level == 1 { + info!("{}", message); + } else if level == 2 { + debug!("{}", message); + } else if level == 3 { + trace!("{}", message); + } else { + warn!( + "Unknown log level: {}, enable DEBUG log level to see more details", + level + ); + debug!("{}", message); + } +} diff --git a/crates/openconnect/src/ffi/vpn.c b/crates/openconnect/src/ffi/vpn.c new file mode 100644 index 0000000..e3d00c3 --- /dev/null +++ b/crates/openconnect/src/ffi/vpn.c @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include +#include + +#include "vpn.h" + +void *g_user_data; + +static int g_cmd_pipe_fd; +static const char *g_vpnc_script; +static vpn_connected_callback on_vpn_connected; + +/* Validate the peer certificate */ +static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason) +{ + INFO("Validating peer cert: %s", reason); + return 0; +} + +/* Print progress messages */ +static void print_progress(__attribute__((unused)) void *_vpninfo, int level, const char *format, ...) +{ + va_list args; + va_start(args, format); + char *message = format_message(format, args); + va_end(args); + + if (message == NULL) + { + ERROR("Failed to format log message"); + } + else + { + LOG(level, message); + free(message); + } +} + +static void setup_tun_handler(void *_vpninfo) +{ + int ret = openconnect_setup_tun_device(_vpninfo, g_vpnc_script, NULL); + if (!ret) { + on_vpn_connected(g_cmd_pipe_fd, g_user_data); + } +} + +/* Initialize VPN connection */ +int vpn_connect(const vpn_options *options, vpn_connected_callback callback) +{ + INFO("openconnect version: %s", openconnect_get_version()); + struct openconnect_info *vpninfo; + struct utsname utsbuf; + + g_user_data = options->user_data; + g_vpnc_script = options->script; + on_vpn_connected = callback; + + INFO("User agent: %s", options->user_agent); + INFO("VPNC script: %s", options->script); + INFO("OS: %s", options->os); + + vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL); + + if (!vpninfo) + { + ERROR("openconnect_vpninfo_new failed"); + return 1; + } + + openconnect_set_loglevel(vpninfo, PRG_TRACE); + openconnect_init_ssl(); + openconnect_set_protocol(vpninfo, "gp"); + openconnect_set_hostname(vpninfo, options->server); + openconnect_set_cookie(vpninfo, options->cookie); + + if (options->os) { + openconnect_set_reported_os(vpninfo, options->os); + } + + if (options->certificate) + { + INFO("Setting client certificate: %s", options->certificate); + openconnect_set_client_cert(vpninfo, options->certificate, NULL); + } + + if (options->servercert) { + INFO("Setting server certificate: %s", options->servercert); + openconnect_set_system_trust(vpninfo, 0); + } + + g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); + if (g_cmd_pipe_fd < 0) + { + ERROR("openconnect_setup_cmd_pipe failed"); + return 1; + } + + if (!uname(&utsbuf)) + { + openconnect_set_localname(vpninfo, utsbuf.nodename); + } + + // Essential step + if (openconnect_make_cstp_connection(vpninfo) != 0) + { + ERROR("openconnect_make_cstp_connection failed"); + return 1; + } + + if (openconnect_setup_dtls(vpninfo, 60) != 0) + { + openconnect_disable_dtls(vpninfo); + } + + // Essential step + openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler); + + while (1) + { + int ret = openconnect_mainloop(vpninfo, 300, 10); + + if (ret) + { + INFO("openconnect_mainloop returned %d, exiting", ret); + openconnect_vpninfo_free(vpninfo); + return ret; + } + + INFO("openconnect_mainloop returned 0, reconnecting"); + } +} + +/* Stop the VPN connection */ +void vpn_disconnect() +{ + char cmd = OC_CMD_CANCEL; + if (write(g_cmd_pipe_fd, &cmd, 1) < 0) + { + ERROR("Failed to write to command pipe, VPN connection may not be stopped"); + } +} diff --git a/crates/openconnect/src/ffi/vpn.h b/crates/openconnect/src/ffi/vpn.h new file mode 100644 index 0000000..91a31d4 --- /dev/null +++ b/crates/openconnect/src/ffi/vpn.h @@ -0,0 +1,68 @@ +#include +#include +#include +#include + +typedef void (*vpn_connected_callback)(int cmd_pipe_fd, void *user_data); + +typedef struct vpn_options +{ + void *user_data; + const char *server; + const char *cookie; + const char *user_agent; + + const char *script; + const char *os; + const char *certificate; + const char *servercert; +} vpn_options; + +int vpn_connect(const vpn_options *options, vpn_connected_callback callback); +void vpn_disconnect(); + +extern void vpn_log(int level, const char *msg); + +static char *format_message(const char *format, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + int len = vsnprintf(NULL, 0, format, args_copy); + va_end(args_copy); + + char *buffer = malloc(len + 1); + if (buffer == NULL) + { + return NULL; + } + + vsnprintf(buffer, len + 1, format, args); + return buffer; +} + +static void _log(int level, ...) +{ + va_list args; + va_start(args, level); + + char *format = va_arg(args, char *); + char *message = format_message(format, args); + + va_end(args); + + if (message == NULL) + { + vpn_log(PRG_ERR, "Failed to format log message"); + } + else + { + vpn_log(level, message); + free(message); + } +} + +#define LOG(level, ...) _log(level, __VA_ARGS__) +#define ERROR(...) LOG(PRG_ERR, __VA_ARGS__) +#define INFO(...) LOG(PRG_INFO, __VA_ARGS__) +#define DEBUG(...) LOG(PRG_DEBUG, __VA_ARGS__) +#define TRACE(...) LOG(PRG_TRACE, __VA_ARGS__) diff --git a/crates/openconnect/src/lib.rs b/crates/openconnect/src/lib.rs new file mode 100644 index 0000000..80977a6 --- /dev/null +++ b/crates/openconnect/src/lib.rs @@ -0,0 +1,5 @@ +mod ffi; +mod vpn; +mod vpnc_script; + +pub use vpn::*; diff --git a/crates/openconnect/src/vpn.rs b/crates/openconnect/src/vpn.rs new file mode 100644 index 0000000..25905a6 --- /dev/null +++ b/crates/openconnect/src/vpn.rs @@ -0,0 +1,131 @@ +use std::{ + ffi::{c_char, CString}, + sync::{Arc, RwLock}, +}; + +use log::info; + +use crate::{ffi, vpnc_script::find_default_vpnc_script}; + +type OnConnectedCallback = Arc>>>; + +pub struct Vpn { + server: CString, + cookie: CString, + user_agent: CString, + script: CString, + os: CString, + certificate: Option, + servercert: Option, + + callback: OnConnectedCallback, +} + +impl Vpn { + pub fn builder(server: &str, cookie: &str) -> VpnBuilder { + VpnBuilder::new(server, cookie) + } + + pub fn connect(&self, on_connected: impl FnOnce() + 'static + Send + Sync) -> i32 { + self + .callback + .write() + .unwrap() + .replace(Box::new(on_connected)); + let options = self.build_connect_options(); + + ffi::connect(&options) + } + + pub(crate) fn on_connected(&self, pipe_fd: i32) { + info!("Connected to VPN, pipe_fd: {}", pipe_fd); + + if let Some(callback) = self.callback.write().unwrap().take() { + callback(); + } + } + + pub fn disconnect(&self) { + ffi::disconnect(); + } + + fn build_connect_options(&self) -> ffi::ConnectOptions { + ffi::ConnectOptions { + user_data: self as *const _ as *mut _, + + server: self.server.as_ptr(), + cookie: self.cookie.as_ptr(), + user_agent: self.user_agent.as_ptr(), + script: self.script.as_ptr(), + os: self.os.as_ptr(), + certificate: Self::option_to_ptr(&self.certificate), + servercert: Self::option_to_ptr(&self.servercert), + } + } + + fn option_to_ptr(option: &Option) -> *const c_char { + match option { + Some(value) => value.as_ptr(), + None => std::ptr::null(), + } + } +} + +pub struct VpnBuilder { + server: String, + cookie: String, + user_agent: Option, + script: Option, + os: Option, +} + +impl VpnBuilder { + fn new(server: &str, cookie: &str) -> Self { + Self { + server: server.to_string(), + cookie: cookie.to_string(), + user_agent: None, + script: None, + os: None, + } + } + + pub fn user_agent>>(mut self, user_agent: T) -> Self { + self.user_agent = user_agent.into(); + self + } + + pub fn script>>(mut self, script: T) -> Self { + self.script = script.into(); + self + } + + pub fn os>>(mut self, os: T) -> Self { + self.os = os.into(); + self + } + + pub fn build(self) -> Vpn { + let user_agent = self.user_agent.unwrap_or_default(); + let script = self + .script + .or_else(find_default_vpnc_script) + .unwrap_or_default(); + let os = self.os.unwrap_or("linux".to_string()); + + Vpn { + server: Self::to_cstring(&self.server), + cookie: Self::to_cstring(&self.cookie), + user_agent: Self::to_cstring(&user_agent), + script: Self::to_cstring(&script), + os: Self::to_cstring(&os), + certificate: None, + servercert: None, + callback: Default::default(), + } + } + + fn to_cstring(value: &str) -> CString { + CString::new(value.to_string()).expect("Failed to convert to CString") + } +} diff --git a/crates/openconnect/src/vpnc_script.rs b/crates/openconnect/src/vpnc_script.rs new file mode 100644 index 0000000..6a34918 --- /dev/null +++ b/crates/openconnect/src/vpnc_script.rs @@ -0,0 +1,23 @@ +use is_executable::IsExecutable; +use std::path::Path; + +const VPNC_SCRIPT_LOCATIONS: [&str; 5] = [ + "/usr/local/share/vpnc-scripts/vpnc-script", + "/usr/local/sbin/vpnc-script", + "/usr/share/vpnc-scripts/vpnc-script", + "/usr/sbin/vpnc-script", + "/etc/vpnc/vpnc-script", +]; + +pub(crate) fn find_default_vpnc_script() -> Option { + for location in VPNC_SCRIPT_LOCATIONS.iter() { + let path = Path::new(location); + if path.is_executable() { + return Some(location.to_string()); + } + } + + log::warn!("vpnc-script not found"); + + None +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..12f98a0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Unix" +reorder_imports = true +reorder_modules = true +edition = "2021" +merge_derives = true