Compare commits
39 Commits
v0.2.1
...
4881b90b0b
Author | SHA1 | Date | |
---|---|---|---|
4881b90b0b | |||
1b749a857c | |||
2079f99d04 | |||
5e0ffc1155 | |||
d4fa8966b2 | |||
a293d8f92c | |||
367a140e2a | |||
4b06dce7f4 | |||
47a3e1cfef | |||
1047818fdc | |||
3d093a3a45 | |||
992d2a4d06 | |||
12f0f187a6 | |||
997e8b419f | |||
1d9132de3b | |||
e1c2618dc8 | |||
a7df7adc8e | |||
03d164c9d3 | |||
f522674a1c | |||
51fcccafa2 | |||
e3913ab4c9 | |||
c16f21bba3 | |||
61d9acc7c6 | |||
8d7b01629d | |||
5685948608 | |||
c98a065587 | |||
e46c3d2b4d | |||
fa228acc3a | |||
e7e0f9d33e | |||
a51b20add7 | |||
890f715388 | |||
89bc74e644 | |||
60c24e3ee4 | |||
486001b584 | |||
52c949e396 | |||
d7c5c2f37b | |||
ae5b8f31db | |||
c260e37e78 | |||
7501253970 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,9 @@ dist
|
|||||||
**/node_modules
|
**/node_modules
|
||||||
src-tauri/target/
|
src-tauri/target/
|
||||||
**/creddy.db
|
**/creddy.db
|
||||||
|
# .env is system-specific
|
||||||
|
.env
|
||||||
|
.vscode
|
||||||
|
|
||||||
# just in case
|
# just in case
|
||||||
credentials*
|
credentials*
|
||||||
|
9
doc/cryptography.md
Normal file
9
doc/cryptography.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
My original plan was to use [libsodium](https://doc.libsodium.org/) to handle encryption. However, the Rust bindings for libsodium are no longer actively maintained, which left me uncomfortable with using it. Instead, I switched to the [RustCrypto](https://github.com/RustCrypto) implementations of the same (or nearly the same) cryptographic primitives provided by libsodium.
|
||||||
|
|
||||||
|
Creddy makes use of two cryptographic primitives: A key-derivation function, which is currently `argon2id`, and a symmetric encryption algorithm, currently `XChaCha20Poly1305`.
|
||||||
|
* I chose `argon2id` because it's what libsodium uses, and because its difficulty parameters admit of very granular tuning.
|
||||||
|
* I chose `XChaCha20Poly1305` because it's _almost_ what libsodium uses - libsodium uses `XSalsa20Poly1305`, and it's my undersatnding that `XChaCha20Poly1305` is an evolution of the former. In both cases I use the eXtended variants, which make use of longer (24-byte) nonces than the non-X variants. This appealed to me because I wanted to be able to randomly generate a nonce every time I needed one, and I have seen [recommendations](https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html) that the 12-byte nonces used by the non-X variants are _juuust_ a touch small for that to be truly worry-free. The RustCrypto implementation of `XChaCha20Poly1305` has also been subject to a security audit, which is nice.
|
||||||
|
|
||||||
|
I tuned the `argon2id` parameters so that key-derivation would take ~800ms on my Ryzen 1600X. This is probably overkill, but I don't intend for key-derivation to be a frequent occurrence - no more than once a day, under normal circumstances. Taking in the neighborhood of 1 second seemed about the longest I could reasonably go.
|
||||||
|
|
||||||
|
**DISCLAIMER**: I am not a professional cryptographer, merely an interested amateur. While I've tried to be as careful as possible with selecting and making use of the cryptographic building blocks I've chosen here, there is always the possibility that I've screwed something up. If anyone would like to sponsor an _actual_ security review of Creddy by people who _actually_ know what they're doing instead of just what they've read on the internet, please let me know.
|
19
doc/todo.md
Normal file
19
doc/todo.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
## Definitely
|
||||||
|
|
||||||
|
* Switch to "process" provider for AWS credentials (much less hacky)
|
||||||
|
* Session timeout (plain duration, or activity-based?)
|
||||||
|
* ~Fix rehide behavior when new request comes in while old one is still being resolved~
|
||||||
|
* Additional hotkey configuration (approve/deny at the very least)
|
||||||
|
* Logging
|
||||||
|
* Icon
|
||||||
|
* Auto-updates
|
||||||
|
* SSH key handling
|
||||||
|
|
||||||
|
## Maybe
|
||||||
|
|
||||||
|
* Flatten error type hierarchy
|
||||||
|
* Rehide after terminal launch from locked
|
||||||
|
* Generalize Request across both credentials and terminal launch?
|
||||||
|
* Make hotkey configuration a little more tolerant of slight mistiming
|
||||||
|
* Distinguish between request that was denied and request that was canceled (e.g. due to error)
|
||||||
|
* Use atomic types for primitive state values instead of RwLock'd types
|
664
package-lock.json
generated
664
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.2.1",
|
"version": "0.3.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
@ -1 +0,0 @@
|
|||||||
DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db
|
|
1444
src-tauri/Cargo.lock
generated
1444
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "creddy"
|
||||||
version = "0.2.1"
|
version = "0.3.4"
|
||||||
description = "A Tauri App"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["you"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
default-run = "app"
|
default-run = "creddy"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.57"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "creddy_cli"
|
||||||
|
path = "src/bin/creddy_cli.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "creddy"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@ -17,12 +25,11 @@ tauri-build = { version = "1.0.4", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["dialog", "os-all", "system-tray"] }
|
tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||||
sodiumoxide = "0.2.7"
|
sodiumoxide = "0.2.7"
|
||||||
tokio = { version = ">=1.19", features = ["full"] }
|
tokio = { version = ">=1.19", features = ["full"] }
|
||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||||
netstat2 = "0.9.1"
|
|
||||||
sysinfo = "0.26.8"
|
sysinfo = "0.26.8"
|
||||||
aws-types = "0.52.0"
|
aws-types = "0.52.0"
|
||||||
aws-sdk-sts = "0.22.0"
|
aws-sdk-sts = "0.22.0"
|
||||||
@ -38,6 +45,8 @@ clap = { version = "3.2.23", features = ["derive"] }
|
|||||||
is-terminal = "0.4.7"
|
is-terminal = "0.4.7"
|
||||||
argon2 = { version = "0.5.0", features = ["std"] }
|
argon2 = { version = "0.5.0", features = ["std"] }
|
||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
|
which = "4.4.0"
|
||||||
|
windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
22
src-tauri/conf/cli.wxs
Normal file
22
src-tauri/conf/cli.wxs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Fragment>
|
||||||
|
|
||||||
|
<DirectoryRef Id="INSTALLDIR">
|
||||||
|
<!-- Create a subdirectory for the console binary so that we can add it to PATH -->
|
||||||
|
<Directory Id="BinDir" Name="bin">
|
||||||
|
<Component Id="CliBinary" Guid="b6358c8e-504f-41fd-b14b-38af821dcd04">
|
||||||
|
<!-- Same name as the main executable, so that it can be invoked as just "creddy" -->
|
||||||
|
<File Id="Bin_Cli" Source="..\..\creddy_cli.exe" Name="creddy.exe" KeyPath="yes"/>
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
<DirectoryRef Id="TARGETDIR">
|
||||||
|
<Component Id="AddToPath" Guid="b5fdaf7e-94f2-4aad-9144-aa3a8edfa675">
|
||||||
|
<Environment Id="CreddyInstallDir" Action="set" Name="PATH" Part="last" Permanent="no" Value="[BinDir]" />
|
||||||
|
</Component>
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
</Wix>
|
@ -19,6 +19,7 @@ use crate::{
|
|||||||
ipc,
|
ipc,
|
||||||
server::Server,
|
server::Server,
|
||||||
errors::*,
|
errors::*,
|
||||||
|
shortcuts,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
tray,
|
tray,
|
||||||
};
|
};
|
||||||
@ -42,6 +43,8 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
ipc::save_credentials,
|
ipc::save_credentials,
|
||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
|
ipc::launch_terminal,
|
||||||
|
ipc::get_setup_errors,
|
||||||
])
|
])
|
||||||
.setup(|app| rt::block_on(setup(app)))
|
.setup(|app| rt::block_on(setup(app)))
|
||||||
.build(tauri::generate_context!())?
|
.build(tauri::generate_context!())?
|
||||||
@ -74,19 +77,45 @@ pub async fn connect_db() -> Result<SqlitePool, SetupError> {
|
|||||||
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
||||||
APP.set(app.handle()).unwrap();
|
APP.set(app.handle()).unwrap();
|
||||||
|
|
||||||
|
// get_or_create_db_path doesn't create the actual db file, just the directory
|
||||||
|
let is_first_launch = !config::get_or_create_db_path()?.exists();
|
||||||
let pool = connect_db().await?;
|
let pool = connect_db().await?;
|
||||||
let conf = AppConfig::load(&pool).await?;
|
let mut setup_errors: Vec<String> = vec![];
|
||||||
|
|
||||||
|
let mut conf = match AppConfig::load(&pool).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(SetupError::ConfigParseError(_)) => {
|
||||||
|
setup_errors.push(
|
||||||
|
"Could not load configuration from database. Reverting to defaults.".into()
|
||||||
|
);
|
||||||
|
AppConfig::default()
|
||||||
|
},
|
||||||
|
err => err?,
|
||||||
|
};
|
||||||
|
|
||||||
let session = Session::load(&pool).await?;
|
let session = Session::load(&pool).await?;
|
||||||
let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
|
Server::start(app.handle())?;
|
||||||
|
|
||||||
config::set_auto_launch(conf.start_on_login)?;
|
config::set_auto_launch(conf.start_on_login)?;
|
||||||
if !conf.start_minimized {
|
if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
|
||||||
|
setup_errors.push("Error: Failed to manage autolaunch.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
|
||||||
|
if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
|
||||||
|
conf.hotkeys.disable_all();
|
||||||
|
conf.save(&pool).await?;
|
||||||
|
setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if session is empty, this is probably the first launch, so don't autohide
|
||||||
|
if !conf.start_minimized || is_first_launch {
|
||||||
app.get_window("main")
|
app.get_window("main")
|
||||||
.ok_or(HandlerError::NoMainWindow)?
|
.ok_or(HandlerError::NoMainWindow)?
|
||||||
.show()?;
|
.show()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = AppState::new(conf, session, srv, pool);
|
let state = AppState::new(conf, session, pool, setup_errors);
|
||||||
app.manage(state);
|
app.manage(state);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
47
src-tauri/src/bin/creddy_cli.rs
Normal file
47
src-tauri/src/bin/creddy_cli.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Windows isn't really amenable to having a single executable work as both a CLI and GUI app,
|
||||||
|
// so we just have a second binary for CLI usage
|
||||||
|
use creddy::{
|
||||||
|
cli,
|
||||||
|
errors::CliError,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
process::{self, Command},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = cli::parser().get_matches();
|
||||||
|
if let Some(true) = args.get_one::<bool>("help") {
|
||||||
|
cli::parser().print_help().unwrap(); // if we can't print help we can't print an error
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = match args.subcommand() {
|
||||||
|
None | Some(("run", _)) => launch_gui(),
|
||||||
|
Some(("get", m)) => cli::get(m),
|
||||||
|
Some(("exec", m)) => cli::exec(m),
|
||||||
|
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||||
|
_ => unreachable!("Unknown subcommand"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
eprintln!("Error: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn launch_gui() -> Result<(), CliError> {
|
||||||
|
let mut path = env::current_exe()?;
|
||||||
|
path.pop(); // bin dir
|
||||||
|
|
||||||
|
// binaries are colocated in dev, but not in production
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
path.pop(); // install dir
|
||||||
|
|
||||||
|
path.push("creddy.exe"); // exe in main install dir (aka gui exe)
|
||||||
|
|
||||||
|
Command::new(path).spawn()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,35 +1,46 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
use std::process::Command as ChildCommand;
|
use std::process::Command as ChildCommand;
|
||||||
#[cfg(unix)]
|
#[cfg(windows)]
|
||||||
use std::os::unix::process::CommandExt;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{
|
use clap::{
|
||||||
Command,
|
Command,
|
||||||
Arg,
|
Arg,
|
||||||
ArgMatches,
|
ArgMatches,
|
||||||
ArgAction
|
ArgAction,
|
||||||
|
builder::PossibleValuesParser,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
net::TcpStream,
|
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
use crate::credentials::Credentials;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::server::{Request, Response};
|
||||||
|
use crate::shortcuts::ShortcutAction;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use {
|
||||||
|
std::os::unix::process::CommandExt,
|
||||||
|
tokio::net::UnixStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
use crate::app;
|
use {
|
||||||
use crate::config::AppConfig;
|
tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions},
|
||||||
use crate::credentials::{BaseCredentials, SessionCredentials};
|
windows::Win32::Foundation::ERROR_PIPE_BUSY,
|
||||||
use crate::errors::*;
|
};
|
||||||
|
|
||||||
|
|
||||||
pub fn parser() -> Command<'static> {
|
pub fn parser() -> Command<'static> {
|
||||||
Command::new("creddy")
|
Command::new("creddy")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("A friendly AWS credentials manager")
|
.about("A friendly AWS credentials manager")
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("run")
|
Command::new("run")
|
||||||
.about("Launch Creddy")
|
.about("Launch Creddy")
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("show")
|
Command::new("get")
|
||||||
.about("Fetch and display AWS credentials")
|
.about("Request AWS credentials from Creddy and output to stdout")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("base")
|
Arg::new("base")
|
||||||
.short('b')
|
.short('b')
|
||||||
@ -54,13 +65,26 @@ pub fn parser() -> Command<'static> {
|
|||||||
.multiple_values(true)
|
.multiple_values(true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("shortcut")
|
||||||
|
.about("Invoke an action normally trigged by hotkey (e.g. launch terminal)")
|
||||||
|
.arg(
|
||||||
|
Arg::new("action")
|
||||||
|
.value_parser(
|
||||||
|
PossibleValuesParser::new(["show_window", "launch_terminal"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn show(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||||
let base = args.get_one("base").unwrap_or(&false);
|
let base = args.get_one("base").unwrap_or(&false);
|
||||||
let creds = get_credentials(*base)?;
|
let output = match get_credentials(*base)? {
|
||||||
println!("{creds}");
|
Credentials::Base(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
|
Credentials::Session(creds) => serde_json::to_string(&creds).unwrap(),
|
||||||
|
};
|
||||||
|
println!("{output}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,31 +98,42 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
let mut cmd = ChildCommand::new(cmd_name);
|
let mut cmd = ChildCommand::new(cmd_name);
|
||||||
cmd.args(cmd_line);
|
cmd.args(cmd_line);
|
||||||
|
|
||||||
if base {
|
match get_credentials(base)? {
|
||||||
let creds: BaseCredentials = serde_json::from_str(&get_credentials(base)?)
|
Credentials::Base(creds) => {
|
||||||
.map_err(|_| RequestError::InvalidJson)?;
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
},
|
||||||
}
|
Credentials::Session(creds) => {
|
||||||
else {
|
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||||
let creds: SessionCredentials = serde_json::from_str(&get_credentials(base)?)
|
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||||
.map_err(|_| RequestError::InvalidJson)?;
|
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
}
|
||||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
|
||||||
cmd.env("AWS_SESSION_TOKEN", creds.token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let e = cmd.exec(); // never returns if successful
|
// cmd.exec() never returns if successful
|
||||||
Err(ExecError::ExecutionFailed(e))?;
|
let e = cmd.exec();
|
||||||
Ok(())
|
match e.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
let name: OsString = cmd_name.into();
|
||||||
|
Err(ExecError::NotFound(name).into())
|
||||||
|
}
|
||||||
|
_ => Err(ExecError::ExecutionFailed(e).into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut child = cmd.spawn()
|
let mut child = match cmd.spawn() {
|
||||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
Ok(c) => c,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
let name: OsString = cmd_name.into();
|
||||||
|
return Err(ExecError::NotFound(name).into());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
|
||||||
|
};
|
||||||
|
|
||||||
let status = child.wait()
|
let status = child.wait()
|
||||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
||||||
std::process::exit(status.code().unwrap_or(1));
|
std::process::exit(status.code().unwrap_or(1));
|
||||||
@ -106,41 +141,63 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||||
async fn get_credentials(base: bool) -> Result<String, RequestError> {
|
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
||||||
let pool = app::connect_db().await?;
|
Some("show_window") => ShortcutAction::ShowWindow,
|
||||||
let config = AppConfig::load(&pool).await?;
|
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
||||||
let path = if base {"/creddy/base-credentials"} else {"/"};
|
Some(&_) | None => unreachable!("Unknown shortcut action"), // guaranteed by clap
|
||||||
|
};
|
||||||
|
|
||||||
let mut stream = TcpStream::connect((config.listen_addr, config.listen_port)).await?;
|
let req = Request::InvokeShortcut(action);
|
||||||
let req = format!("GET {path} HTTP/1.0\r\n\r\n");
|
match make_request(&req) {
|
||||||
stream.write_all(req.as_bytes()).await?;
|
Ok(Response::Empty) => Ok(()),
|
||||||
|
Ok(r) => Err(RequestError::Unexpected(r).into()),
|
||||||
// some day we'll have a proper HTTP parser
|
Err(e) => Err(e.into()),
|
||||||
let mut buf = vec![0; 8192];
|
|
||||||
stream.read_to_end(&mut buf).await?;
|
|
||||||
|
|
||||||
let status = buf.split(|&c| &[c] == b" ")
|
|
||||||
.skip(1)
|
|
||||||
.next()
|
|
||||||
.ok_or(RequestError::MalformedHttpResponse)?;
|
|
||||||
|
|
||||||
if status != b"200" {
|
|
||||||
let s = String::from_utf8_lossy(status).to_string();
|
|
||||||
return Err(RequestError::Failed(s));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let break_idx = buf.windows(4)
|
|
||||||
.position(|w| w == b"\r\n\r\n")
|
|
||||||
.ok_or(RequestError::MalformedHttpResponse)?;
|
fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
|
||||||
let body = &buf[(break_idx + 4)..];
|
let req = Request::GetAwsCredentials { base };
|
||||||
|
match make_request(&req) {
|
||||||
let creds_str = std::str::from_utf8(body)
|
Ok(Response::Aws(creds)) => Ok(creds),
|
||||||
.map_err(|_| RequestError::MalformedHttpResponse)?
|
Ok(r) => Err(RequestError::Unexpected(r)),
|
||||||
.to_string();
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
if creds_str == "Denied!" {
|
}
|
||||||
return Err(RequestError::Rejected);
|
|
||||||
}
|
|
||||||
Ok(creds_str)
|
#[tokio::main]
|
||||||
|
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
||||||
|
let mut data = serde_json::to_string(req).unwrap();
|
||||||
|
// server expects newline marking end of request
|
||||||
|
data.push('\n');
|
||||||
|
|
||||||
|
let mut stream = connect().await?;
|
||||||
|
stream.write_all(&data.as_bytes()).await?;
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(1024);
|
||||||
|
stream.read_to_end(&mut buf).await?;
|
||||||
|
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
|
||||||
|
Ok(res?)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
||||||
|
// apparently attempting to connect can fail if there's already a client connected
|
||||||
|
loop {
|
||||||
|
match ClientOptions::new().open(r"\\.\pipe\creddy-requests") {
|
||||||
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn connect() -> Result<UnixStream, std::io::Error> {
|
||||||
|
UnixStream::connect("/tmp/creddy.sock").await
|
||||||
}
|
}
|
||||||
|
@ -1,76 +1,92 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
|
|
||||||
use tauri::Manager;
|
|
||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::errors::*;
|
||||||
app::APP,
|
|
||||||
errors::*,
|
|
||||||
config::AppConfig,
|
|
||||||
state::AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
pub pid: u32,
|
pub pid: u32,
|
||||||
pub exe: PathBuf,
|
pub exe: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
dbg!(pid);
|
||||||
let AppConfig {
|
let sys_pid = Pid::from_u32(pid);
|
||||||
listen_addr: app_listen_addr,
|
let mut sys = System::new();
|
||||||
listen_port: app_listen_port,
|
sys.refresh_process(sys_pid);
|
||||||
..
|
let proc = sys.process(sys_pid)
|
||||||
} = *state.config.read().await;
|
.ok_or(ClientInfoError::ProcessNotFound)?;
|
||||||
|
|
||||||
let sockets_iter = netstat2::iterate_sockets_info(
|
let parent_pid_sys = proc.parent()
|
||||||
AddressFamilyFlags::IPV4,
|
.ok_or(ClientInfoError::ParentPidNotFound)?;
|
||||||
ProtocolFlags::TCP
|
sys.refresh_process(parent_pid_sys);
|
||||||
)?;
|
let parent = sys.process(parent_pid_sys)
|
||||||
for item in sockets_iter {
|
.ok_or(ClientInfoError::ParentProcessNotFound)?;
|
||||||
let sock_info = item?;
|
|
||||||
let proto_info = match sock_info.protocol_socket_info {
|
|
||||||
ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
|
|
||||||
ProtocolSocketInfo::Udp(_) => {continue;}
|
|
||||||
};
|
|
||||||
|
|
||||||
if proto_info.local_port == local_port
|
let exe = match parent.exe() {
|
||||||
&& proto_info.remote_port == app_listen_port
|
p if p == Path::new("") => None,
|
||||||
&& proto_info.local_addr == app_listen_addr
|
p => Some(PathBuf::from(p)),
|
||||||
&& proto_info.remote_addr == app_listen_addr
|
};
|
||||||
{
|
|
||||||
return Ok(sock_info.associated_pids)
|
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
||||||
|
// let state = APP.get().unwrap().state::<AppState>();
|
||||||
|
// let AppConfig {
|
||||||
|
// listen_addr: app_listen_addr,
|
||||||
|
// listen_port: app_listen_port,
|
||||||
|
// ..
|
||||||
|
// } = *state.config.read().await;
|
||||||
|
|
||||||
|
// let sockets_iter = netstat2::iterate_sockets_info(
|
||||||
|
// AddressFamilyFlags::IPV4,
|
||||||
|
// ProtocolFlags::TCP
|
||||||
|
// )?;
|
||||||
|
// for item in sockets_iter {
|
||||||
|
// let sock_info = item?;
|
||||||
|
// let proto_info = match sock_info.protocol_socket_info {
|
||||||
|
// ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
|
||||||
|
// ProtocolSocketInfo::Udp(_) => {continue;}
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if proto_info.local_port == local_port
|
||||||
|
// && proto_info.remote_port == app_listen_port
|
||||||
|
// && proto_info.local_addr == app_listen_addr
|
||||||
|
// && proto_info.remote_addr == app_listen_addr
|
||||||
|
// {
|
||||||
|
// return Ok(sock_info.associated_pids)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Ok(vec![])
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
// Theoretically, on some systems, multiple processes can share a socket
|
// Theoretically, on some systems, multiple processes can share a socket
|
||||||
pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
// pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
||||||
let mut clients = Vec::new();
|
// let mut clients = Vec::new();
|
||||||
let mut sys = System::new();
|
// let mut sys = System::new();
|
||||||
for p in get_associated_pids(local_port).await? {
|
// for p in get_associated_pids(local_port).await? {
|
||||||
let pid = Pid::from_u32(p);
|
// let pid = Pid::from_u32(p);
|
||||||
sys.refresh_process(pid);
|
// sys.refresh_process(pid);
|
||||||
let proc = sys.process(pid)
|
// let proc = sys.process(pid)
|
||||||
.ok_or(ClientInfoError::ProcessNotFound)?;
|
// .ok_or(ClientInfoError::ProcessNotFound)?;
|
||||||
|
|
||||||
let client = Client {
|
// let client = Client {
|
||||||
pid: p,
|
// pid: p,
|
||||||
exe: proc.exe().to_path_buf(),
|
// exe: proc.exe().to_path_buf(),
|
||||||
};
|
// };
|
||||||
clients.push(Some(client));
|
// clients.push(Some(client));
|
||||||
}
|
// }
|
||||||
|
|
||||||
if clients.is_empty() {
|
// if clients.is_empty() {
|
||||||
clients.push(None);
|
// clients.push(None);
|
||||||
}
|
// }
|
||||||
|
|
||||||
Ok(clients)
|
// Ok(clients)
|
||||||
}
|
// }
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use std::net::Ipv4Addr;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use auto_launch::AutoLaunchBuilder;
|
use auto_launch::AutoLaunchBuilder;
|
||||||
@ -9,29 +8,62 @@ use sqlx::SqlitePool;
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TermConfig {
|
||||||
|
pub name: String,
|
||||||
|
// we call it exec because it isn't always the actual path,
|
||||||
|
// in some cases it's just the name and relies on path-searching
|
||||||
|
// it's a string because it can come from the frontend as json
|
||||||
|
pub exec: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct Hotkey {
|
||||||
|
pub keys: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct HotkeysConfig {
|
||||||
|
// tauri uses strings to represent keybinds, so we will as well
|
||||||
|
pub show_window: Hotkey,
|
||||||
|
pub launch_terminal: Hotkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HotkeysConfig {
|
||||||
|
pub fn disable_all(&mut self) {
|
||||||
|
self.show_window.enabled = false;
|
||||||
|
self.launch_terminal.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
#[serde(default = "default_listen_addr")]
|
|
||||||
pub listen_addr: Ipv4Addr,
|
|
||||||
#[serde(default = "default_listen_port")]
|
|
||||||
pub listen_port: u16,
|
|
||||||
#[serde(default = "default_rehide_ms")]
|
#[serde(default = "default_rehide_ms")]
|
||||||
pub rehide_ms: u64,
|
pub rehide_ms: u64,
|
||||||
#[serde(default = "default_start_minimized")]
|
#[serde(default = "default_start_minimized")]
|
||||||
pub start_minimized: bool,
|
pub start_minimized: bool,
|
||||||
#[serde(default = "default_start_on_login")]
|
#[serde(default = "default_start_on_login")]
|
||||||
pub start_on_login: bool,
|
pub start_on_login: bool,
|
||||||
|
#[serde(default = "default_term_config")]
|
||||||
|
pub terminal: TermConfig,
|
||||||
|
#[serde(default = "default_hotkey_config")]
|
||||||
|
pub hotkeys: HotkeysConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
listen_addr: default_listen_addr(),
|
|
||||||
listen_port: default_listen_port(),
|
|
||||||
rehide_ms: default_rehide_ms(),
|
rehide_ms: default_rehide_ms(),
|
||||||
start_minimized: default_start_minimized(),
|
start_minimized: default_start_minimized(),
|
||||||
start_on_login: default_start_on_login(),
|
start_on_login: default_start_on_login(),
|
||||||
|
terminal: default_term_config(),
|
||||||
|
hotkeys: default_hotkey_config(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,16 +139,53 @@ pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn default_listen_port() -> u16 {
|
fn default_term_config() -> TermConfig {
|
||||||
if cfg!(debug_assertions) {
|
#[cfg(windows)]
|
||||||
12_345
|
{
|
||||||
|
let shell = if which::which("pwsh.exe").is_ok() {
|
||||||
|
"pwsh.exe".to_string()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"powershell.exe".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (exec, args) = if cfg!(debug_assertions) {
|
||||||
|
("conhost.exe".to_string(), vec![shell.clone()])
|
||||||
|
} else {
|
||||||
|
(shell.clone(), vec![])
|
||||||
|
};
|
||||||
|
|
||||||
|
TermConfig { name: shell, exec, args }
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
19_923
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
for bin in ["gnome-terminal", "konsole"] {
|
||||||
|
if let Ok(_) = which::which(bin) {
|
||||||
|
return TermConfig {
|
||||||
|
name: bin.into(),
|
||||||
|
exec: bin.into(),
|
||||||
|
args: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TermConfig {
|
||||||
|
name: "gnome-terminal".into(),
|
||||||
|
exec: "gnome-terminal".into(),
|
||||||
|
args: vec![],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
|
||||||
|
fn default_hotkey_config() -> HotkeysConfig {
|
||||||
|
HotkeysConfig {
|
||||||
|
show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true},
|
||||||
|
launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn default_rehide_ms() -> u64 { 1000 }
|
fn default_rehide_ms() -> u64 { 1000 }
|
||||||
// start minimized and on login only in production mode
|
// start minimized and on login only in production mode
|
||||||
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
|
||||||
|
@ -81,6 +81,16 @@ impl Session {
|
|||||||
Session::Empty => Err(GetSessionError::CredentialsEmpty),
|
Session::Empty => Err(GetSessionError::CredentialsEmpty),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn try_get(
|
||||||
|
&self
|
||||||
|
) -> Result<(&BaseCredentials, &SessionCredentials), GetCredentialsError> {
|
||||||
|
match self {
|
||||||
|
Self::Empty => Err(GetCredentialsError::Empty),
|
||||||
|
Self::Locked(_) => Err(GetCredentialsError::Locked),
|
||||||
|
Self::Unlocked{ ref base, ref session } => Ok((base, session))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -152,9 +162,10 @@ impl BaseCredentials {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct SessionCredentials {
|
pub struct SessionCredentials {
|
||||||
|
pub version: usize,
|
||||||
pub access_key_id: String,
|
pub access_key_id: String,
|
||||||
pub secret_access_key: String,
|
pub secret_access_key: String,
|
||||||
pub token: String,
|
pub session_token: String,
|
||||||
#[serde(serialize_with = "serialize_expiration")]
|
#[serde(serialize_with = "serialize_expiration")]
|
||||||
#[serde(deserialize_with = "deserialize_expiration")]
|
#[serde(deserialize_with = "deserialize_expiration")]
|
||||||
pub expiration: DateTime,
|
pub expiration: DateTime,
|
||||||
@ -188,7 +199,7 @@ impl SessionCredentials {
|
|||||||
let secret_access_key = aws_session.secret_access_key()
|
let secret_access_key = aws_session.secret_access_key()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::EmptyResponse)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let token = aws_session.session_token()
|
let session_token = aws_session.session_token()
|
||||||
.ok_or(GetSessionError::EmptyResponse)?
|
.ok_or(GetSessionError::EmptyResponse)?
|
||||||
.to_string();
|
.to_string();
|
||||||
let expiration = aws_session.expiration()
|
let expiration = aws_session.expiration()
|
||||||
@ -196,9 +207,10 @@ impl SessionCredentials {
|
|||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let session_creds = SessionCredentials {
|
let session_creds = SessionCredentials {
|
||||||
|
version: 1,
|
||||||
access_key_id,
|
access_key_id,
|
||||||
secret_access_key,
|
secret_access_key,
|
||||||
token,
|
session_token,
|
||||||
expiration,
|
expiration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,6 +232,14 @@ impl SessionCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Credentials {
|
||||||
|
Base(BaseCredentials),
|
||||||
|
Session(SessionCredentials),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where S: Serializer
|
where S: Serializer
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
use strum_macros::AsRefStr;
|
use strum_macros::AsRefStr;
|
||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
@ -16,14 +18,22 @@ use tauri::api::dialog::{
|
|||||||
MessageDialogBuilder,
|
MessageDialogBuilder,
|
||||||
MessageDialogKind,
|
MessageDialogKind,
|
||||||
};
|
};
|
||||||
use serde::{Serialize, Serializer, ser::SerializeMap};
|
use serde::{
|
||||||
|
Serialize,
|
||||||
|
Serializer,
|
||||||
|
ser::SerializeMap,
|
||||||
|
Deserialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
pub trait ErrorPopup {
|
pub trait ShowError {
|
||||||
fn error_popup(self, title: &str);
|
fn error_popup(self, title: &str);
|
||||||
|
fn error_popup_nowait(self, title: &str);
|
||||||
|
fn error_print(self);
|
||||||
|
fn error_print_prefix(self, prefix: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Error> ErrorPopup for Result<(), E> {
|
impl<E: std::fmt::Display> ShowError for Result<(), E> {
|
||||||
fn error_popup(self, title: &str) {
|
fn error_popup(self, title: &str) {
|
||||||
if let Err(e) = self {
|
if let Err(e) = self {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
@ -34,6 +44,26 @@ impl<E: Error> ErrorPopup for Result<(), E> {
|
|||||||
rx.recv().unwrap();
|
rx.recv().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn error_popup_nowait(self, title: &str) {
|
||||||
|
if let Err(e) = self {
|
||||||
|
MessageDialogBuilder::new(title, format!("{e}"))
|
||||||
|
.kind(MessageDialogKind::Error)
|
||||||
|
.show(|_| {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_print(self) {
|
||||||
|
if let Err(e) = self {
|
||||||
|
eprintln!("{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_print_prefix(self, prefix: &str) {
|
||||||
|
if let Err(e) = self {
|
||||||
|
eprintln!("{prefix}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -57,8 +87,12 @@ where
|
|||||||
E: Error,
|
E: Error,
|
||||||
M: serde::ser::SerializeMap,
|
M: serde::ser::SerializeMap,
|
||||||
{
|
{
|
||||||
let src = err.source().map(|s| format!("{s}"));
|
let msg = err.source().map(|s| format!("{s}"));
|
||||||
map.serialize_entry("source", &src)
|
map.serialize_entry("msg", &msg)?;
|
||||||
|
map.serialize_entry("code", &None::<&str>)?;
|
||||||
|
map.serialize_entry("source", &None::<&str>)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +124,8 @@ pub enum SetupError {
|
|||||||
ServerSetupError(#[from] std::io::Error),
|
ServerSetupError(#[from] std::io::Error),
|
||||||
#[error("Failed to resolve data directory: {0}")]
|
#[error("Failed to resolve data directory: {0}")]
|
||||||
DataDir(#[from] DataDirError),
|
DataDir(#[from] DataDirError),
|
||||||
|
#[error("Failed to register hotkeys: {0}")]
|
||||||
|
RegisterHotkeys(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +145,8 @@ pub enum SendResponseError {
|
|||||||
NotFound,
|
NotFound,
|
||||||
#[error("The specified request was already closed by the client")]
|
#[error("The specified request was already closed by the client")]
|
||||||
Abandoned,
|
Abandoned,
|
||||||
|
#[error("A response has already been received for the specified request")]
|
||||||
|
Fulfilled,
|
||||||
#[error("Could not renew AWS sesssion: {0}")]
|
#[error("Could not renew AWS sesssion: {0}")]
|
||||||
SessionRenew(#[from] GetSessionError),
|
SessionRenew(#[from] GetSessionError),
|
||||||
}
|
}
|
||||||
@ -119,12 +157,14 @@ pub enum SendResponseError {
|
|||||||
pub enum HandlerError {
|
pub enum HandlerError {
|
||||||
#[error("Error writing to stream: {0}")]
|
#[error("Error writing to stream: {0}")]
|
||||||
StreamIOError(#[from] std::io::Error),
|
StreamIOError(#[from] std::io::Error),
|
||||||
// #[error("Received invalid UTF-8 in request")]
|
#[error("Received invalid UTF-8 in request")]
|
||||||
// InvalidUtf8,
|
InvalidUtf8(#[from] FromUtf8Error),
|
||||||
#[error("HTTP request malformed")]
|
#[error("HTTP request malformed")]
|
||||||
BadRequest(Vec<u8>),
|
BadRequest(#[from] serde_json::Error),
|
||||||
#[error("HTTP request too large")]
|
#[error("HTTP request too large")]
|
||||||
RequestTooLarge,
|
RequestTooLarge,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
Internal,
|
||||||
#[error("Error accessing credentials: {0}")]
|
#[error("Error accessing credentials: {0}")]
|
||||||
NoCredentials(#[from] GetCredentialsError),
|
NoCredentials(#[from] GetCredentialsError),
|
||||||
#[error("Error getting client details: {0}")]
|
#[error("Error getting client details: {0}")]
|
||||||
@ -133,6 +173,17 @@ pub enum HandlerError {
|
|||||||
Tauri(#[from] tauri::Error),
|
Tauri(#[from] tauri::Error),
|
||||||
#[error("No main application window found")]
|
#[error("No main application window found")]
|
||||||
NoMainWindow,
|
NoMainWindow,
|
||||||
|
#[error("Request was denied")]
|
||||||
|
Denied,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum WindowError {
|
||||||
|
#[error("Failed to find main application window")]
|
||||||
|
NoMainWindow,
|
||||||
|
#[error(transparent)]
|
||||||
|
ManageFailure(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -189,36 +240,50 @@ pub enum CryptoError {
|
|||||||
pub enum ClientInfoError {
|
pub enum ClientInfoError {
|
||||||
#[error("Found PID for client socket, but no corresponding process")]
|
#[error("Found PID for client socket, but no corresponding process")]
|
||||||
ProcessNotFound,
|
ProcessNotFound,
|
||||||
#[error("Couldn't get client socket details: {0}")]
|
#[error("Could not determine parent PID of connected client")]
|
||||||
NetstatError(#[from] netstat2::error::Error),
|
ParentPidNotFound,
|
||||||
|
#[error("Found PID for parent process of client, but no corresponding process")]
|
||||||
|
ParentProcessNotFound,
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[error("Could not determine PID of connected client")]
|
||||||
|
WindowsError(#[from] windows::core::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Technically also an error, but formatted as a struct for easy deserialization
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ServerError {
|
||||||
|
code: String,
|
||||||
|
msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ServerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "{} ({})", self.msg, self.code)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
|
// Errors encountered while requesting credentials via CLI (creddy show, creddy exec)
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
pub enum RequestError {
|
pub enum RequestError {
|
||||||
#[error("Credentials request failed: HTTP {0}")]
|
#[error("Error response from server: {0}")]
|
||||||
Failed(String),
|
Server(ServerError),
|
||||||
#[error("Credentials request was rejected")]
|
#[error("Unexpected response from server")]
|
||||||
Rejected,
|
Unexpected(crate::server::Response),
|
||||||
#[error("Couldn't interpret the server's response")]
|
|
||||||
MalformedHttpResponse,
|
|
||||||
#[error("The server did not respond with valid JSON")]
|
#[error("The server did not respond with valid JSON")]
|
||||||
InvalidJson,
|
InvalidJson(#[from] serde_json::Error),
|
||||||
#[error("Error reading/writing stream: {0}")]
|
#[error("Error reading/writing stream: {0}")]
|
||||||
StreamIOError(#[from] std::io::Error),
|
StreamIOError(#[from] std::io::Error),
|
||||||
#[error("Error loading configuration data: {0}")]
|
|
||||||
Setup(#[from] SetupError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ServerError> for RequestError {
|
||||||
// Errors encountered while running a subprocess (via creddy exec)
|
fn from(s: ServerError) -> Self {
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
Self::Server(s)
|
||||||
pub enum ExecError {
|
}
|
||||||
#[error("Please specify a command")]
|
|
||||||
NoCommand,
|
|
||||||
#[error("Failed to execute command: {0}")]
|
|
||||||
ExecutionFailed(#[from] std::io::Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -228,6 +293,35 @@ pub enum CliError {
|
|||||||
Request(#[from] RequestError),
|
Request(#[from] RequestError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Exec(#[from] ExecError),
|
Exec(#[from] ExecError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Errors encountered while trying to launch a child process
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum ExecError {
|
||||||
|
#[error("Please specify a command")]
|
||||||
|
NoCommand,
|
||||||
|
#[error("Executable not found: {0:?}")]
|
||||||
|
NotFound(OsString),
|
||||||
|
#[error("Failed to execute command: {0}")]
|
||||||
|
ExecutionFailed(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LaunchTerminalError {
|
||||||
|
#[error("Could not discover main window")]
|
||||||
|
NoMainWindow,
|
||||||
|
#[error("Failed to communicate with main Creddy window")]
|
||||||
|
IpcFailed(#[from] tauri::Error),
|
||||||
|
#[error("Failed to launch terminal: {0}")]
|
||||||
|
Exec(#[from] ExecError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -254,6 +348,7 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
|||||||
impl_serialize_basic!(SetupError);
|
impl_serialize_basic!(SetupError);
|
||||||
impl_serialize_basic!(GetCredentialsError);
|
impl_serialize_basic!(GetCredentialsError);
|
||||||
impl_serialize_basic!(ClientInfoError);
|
impl_serialize_basic!(ClientInfoError);
|
||||||
|
impl_serialize_basic!(WindowError);
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for HandlerError {
|
impl Serialize for HandlerError {
|
||||||
@ -261,13 +356,6 @@ impl Serialize for HandlerError {
|
|||||||
let mut map = serializer.serialize_map(None)?;
|
let mut map = serializer.serialize_map(None)?;
|
||||||
map.serialize_entry("code", self.as_ref())?;
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
map.serialize_entry("msg", &format!("{self}"))?;
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
match self {
|
|
||||||
HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?,
|
|
||||||
HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?,
|
|
||||||
_ => serialize_upstream_err(self, &mut map)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,6 +404,38 @@ impl Serialize for UnlockError {
|
|||||||
|
|
||||||
match self {
|
match self {
|
||||||
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
|
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
|
||||||
|
// The string representation of the AEAD error is not very helpful, so skip it
|
||||||
|
UnlockError::Crypto(_src) => map.serialize_entry("source", &None::<&str>)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for ExecError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ExecError::GetCredentials(src) => map.serialize_entry("source", &src)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for LaunchTerminalError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?,
|
||||||
_ => serialize_upstream_err(self, &mut map)?,
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
}
|
}
|
||||||
map.end()
|
map.end()
|
||||||
|
@ -6,12 +6,13 @@ use crate::credentials::{Session,BaseCredentials};
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
use crate::terminal;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Request {
|
pub struct AwsRequestNotification {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub clients: Vec<Option<Client>>,
|
pub client: Client,
|
||||||
pub base: bool,
|
pub base: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,3 +79,15 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R
|
|||||||
.map_err(|e| format!("Error saving config: {e}"))?;
|
.map_err(|e| format!("Error saving config: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
|
terminal::launch(base).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<String>, ()> {
|
||||||
|
Ok(app_state.setup_errors.clone())
|
||||||
|
}
|
||||||
|
12
src-tauri/src/lib.rs
Normal file
12
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod cli;
|
||||||
|
mod config;
|
||||||
|
mod credentials;
|
||||||
|
pub mod errors;
|
||||||
|
mod clientinfo;
|
||||||
|
mod ipc;
|
||||||
|
mod state;
|
||||||
|
mod server;
|
||||||
|
mod shortcuts;
|
||||||
|
mod terminal;
|
||||||
|
mod tray;
|
@ -3,20 +3,11 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
use creddy::{
|
||||||
mod app;
|
app,
|
||||||
mod cli;
|
cli,
|
||||||
mod config;
|
errors::ShowError,
|
||||||
mod credentials;
|
};
|
||||||
mod errors;
|
|
||||||
mod clientinfo;
|
|
||||||
mod ipc;
|
|
||||||
mod state;
|
|
||||||
mod server;
|
|
||||||
mod tray;
|
|
||||||
|
|
||||||
|
|
||||||
use crate::errors::ErrorPopup;
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -25,12 +16,14 @@ fn main() {
|
|||||||
app::run().error_popup("Creddy failed to start");
|
app::run().error_popup("Creddy failed to start");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Some(("show", m)) => cli::show(m),
|
Some(("get", m)) => cli::get(m),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m),
|
||||||
|
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,243 +0,0 @@
|
|||||||
use core::time::Duration;
|
|
||||||
use std::io;
|
|
||||||
use std::net::{
|
|
||||||
Ipv4Addr,
|
|
||||||
SocketAddr,
|
|
||||||
SocketAddrV4,
|
|
||||||
};
|
|
||||||
use tokio::net::{
|
|
||||||
TcpListener,
|
|
||||||
TcpStream,
|
|
||||||
};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
|
||||||
use tauri::async_runtime as rt;
|
|
||||||
use tauri::async_runtime::JoinHandle;
|
|
||||||
|
|
||||||
use crate::{clientinfo, clientinfo::Client};
|
|
||||||
use crate::errors::*;
|
|
||||||
use crate::ipc::{Request, Approval};
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
|
|
||||||
struct Handler {
|
|
||||||
request_id: u64,
|
|
||||||
stream: TcpStream,
|
|
||||||
receiver: Option<oneshot::Receiver<Approval>>,
|
|
||||||
app: AppHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler {
|
|
||||||
async fn new(stream: TcpStream, app: AppHandle) -> Self {
|
|
||||||
let state = app.state::<AppState>();
|
|
||||||
let (chan_send, chan_recv) = oneshot::channel();
|
|
||||||
let request_id = state.register_request(chan_send).await;
|
|
||||||
Handler {
|
|
||||||
request_id,
|
|
||||||
stream,
|
|
||||||
receiver: Some(chan_recv),
|
|
||||||
app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(mut self) {
|
|
||||||
if let Err(e) = self.try_handle().await {
|
|
||||||
eprintln!("{e}");
|
|
||||||
}
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
state.unregister_request(self.request_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_handle(&mut self) -> Result<(), HandlerError> {
|
|
||||||
let req_path = self.recv_request().await?;
|
|
||||||
let clients = self.get_clients().await?;
|
|
||||||
if self.includes_banned(&clients).await {
|
|
||||||
self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
let base = req_path == b"/creddy/base-credentials";
|
|
||||||
|
|
||||||
let req = Request {id: self.request_id, clients, base};
|
|
||||||
self.app.emit_all("credentials-request", &req)?;
|
|
||||||
let starting_visibility = self.show_window()?;
|
|
||||||
|
|
||||||
match self.wait_for_response().await? {
|
|
||||||
Approval::Approved => {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
let creds = if base {
|
|
||||||
state.serialize_base_creds().await?
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.serialize_session_creds().await?
|
|
||||||
};
|
|
||||||
self.send_body(creds.as_bytes()).await?;
|
|
||||||
},
|
|
||||||
Approval::Denied => {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
for client in req.clients {
|
|
||||||
state.add_ban(client).await;
|
|
||||||
}
|
|
||||||
self.send_body(b"Denied!").await?;
|
|
||||||
self.stream.shutdown().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only hide the window if a) it was hidden to start with
|
|
||||||
// and b) there are no other pending requests
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
let delay = {
|
|
||||||
let config = state.config.read().await;
|
|
||||||
Duration::from_millis(config.rehide_ms)
|
|
||||||
};
|
|
||||||
sleep(delay).await;
|
|
||||||
|
|
||||||
if !starting_visibility && state.req_count().await == 0 {
|
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
|
||||||
window.hide()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv_request(&mut self) -> Result<Vec<u8>, HandlerError> {
|
|
||||||
let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
|
|
||||||
let mut n = 0;
|
|
||||||
loop {
|
|
||||||
n += self.stream.read(&mut buf[n..]).await?;
|
|
||||||
if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
|
|
||||||
if n == buf.len() {return Err(HandlerError::RequestTooLarge);}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = buf.split(|&c| &[c] == b" ")
|
|
||||||
.skip(1)
|
|
||||||
.next()
|
|
||||||
.ok_or(HandlerError::BadRequest(buf.clone()))?;
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)] {
|
|
||||||
println!("Path: {}", std::str::from_utf8(&path).unwrap());
|
|
||||||
println!("{}", std::str::from_utf8(&buf).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(path.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_clients(&self) -> Result<Vec<Option<Client>>, HandlerError> {
|
|
||||||
let peer_addr = match self.stream.peer_addr()? {
|
|
||||||
SocketAddr::V4(addr) => addr,
|
|
||||||
_ => unreachable!(), // we only listen on IPv4
|
|
||||||
};
|
|
||||||
let clients = clientinfo::get_clients(peer_addr.port()).await?;
|
|
||||||
Ok(clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
|
|
||||||
let state = self.app.state::<AppState>();
|
|
||||||
for client in clients {
|
|
||||||
if state.is_banned(client).await {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_window(&self) -> Result<bool, HandlerError> {
|
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
|
||||||
let starting_visibility = window.is_visible()?;
|
|
||||||
if !starting_visibility {
|
|
||||||
window.unminimize()?;
|
|
||||||
window.show()?;
|
|
||||||
}
|
|
||||||
window.set_focus()?;
|
|
||||||
Ok(starting_visibility)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
|
|
||||||
self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
|
|
||||||
self.stream.write(b"Content-Type: application/json\r\n").await?;
|
|
||||||
self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
|
|
||||||
|
|
||||||
#[allow(unreachable_code)] // seems necessary for type inference
|
|
||||||
let stall = async {
|
|
||||||
let delay = std::time::Duration::from_secs(1);
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
self.stream.write(b"x").await?;
|
|
||||||
}
|
|
||||||
Ok(Approval::Denied)
|
|
||||||
};
|
|
||||||
|
|
||||||
// this is the only place we even read this field, so it's safe to unwrap
|
|
||||||
let receiver = self.receiver.take().unwrap();
|
|
||||||
tokio::select!{
|
|
||||||
r = receiver => Ok(r.unwrap()), // only panics if the sender is dropped without sending, which shouldn't be possible
|
|
||||||
e = stall => e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_body(&mut self, body: &[u8]) -> Result<(), HandlerError> {
|
|
||||||
self.stream.write(b"\r\nContent-Length: ").await?;
|
|
||||||
self.stream.write(body.len().to_string().as_bytes()).await?;
|
|
||||||
self.stream.write(b"\r\n\r\n").await?;
|
|
||||||
self.stream.write(body).await?;
|
|
||||||
self.stream.shutdown().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Server {
|
|
||||||
addr: Ipv4Addr,
|
|
||||||
port: u16,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
task: JoinHandle<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<Server> {
|
|
||||||
let task = Self::start_server(addr, port, app_handle.app_handle()).await?;
|
|
||||||
Ok(Server { addr, port, app_handle, task})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn rebind(&mut self, addr: Ipv4Addr, port: u16) -> io::Result<()> {
|
|
||||||
if addr == self.addr && port == self.port {
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_task = Self::start_server(addr, port, self.app_handle.app_handle()).await?;
|
|
||||||
self.task.abort();
|
|
||||||
|
|
||||||
self.addr = addr;
|
|
||||||
self.port = port;
|
|
||||||
self.task = new_task;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct the listener before spawning the task so that we can return early if it fails
|
|
||||||
async fn start_server(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<JoinHandle<()>> {
|
|
||||||
let sock_addr = SocketAddrV4::new(addr, port);
|
|
||||||
let listener = TcpListener::bind(&sock_addr).await?;
|
|
||||||
let task = rt::spawn(
|
|
||||||
Self::serve(listener, app_handle.app_handle())
|
|
||||||
);
|
|
||||||
Ok(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve(listener: TcpListener, app_handle: AppHandle) {
|
|
||||||
loop {
|
|
||||||
match listener.accept().await {
|
|
||||||
Ok((stream, _)) => {
|
|
||||||
let handler = Handler::new(stream, app_handle.app_handle()).await;
|
|
||||||
rt::spawn(handler.handle());
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error accepting connection: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
126
src-tauri/src/server/mod.rs
Normal file
126
src-tauri/src/server/mod.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::clientinfo::{self, Client};
|
||||||
|
use crate::credentials::Credentials;
|
||||||
|
use crate::ipc::{Approval, AwsRequestNotification};
|
||||||
|
use crate::state::AppState;
|
||||||
|
use crate::shortcuts::{self, ShortcutAction};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod server_win;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use server_win::Server;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use server_win::Stream;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod server_unix;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use server_unix::Server;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use server_unix::Stream;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum Request {
|
||||||
|
GetAwsCredentials{
|
||||||
|
base: bool,
|
||||||
|
},
|
||||||
|
InvokeShortcut(ShortcutAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Response {
|
||||||
|
Aws(Credentials),
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
||||||
|
{
|
||||||
|
// read from stream until delimiter is reached
|
||||||
|
let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
|
||||||
|
let mut n = 0;
|
||||||
|
loop {
|
||||||
|
n += stream.read_buf(&mut buf).await?;
|
||||||
|
if let Some(&b'\n') = buf.last() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if n >= 1024 {
|
||||||
|
return Err(HandlerError::RequestTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = clientinfo::get_process_parent_info(client_pid)?;
|
||||||
|
|
||||||
|
let req: Request = serde_json::from_slice(&buf)?;
|
||||||
|
let res = match req {
|
||||||
|
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
||||||
|
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = serde_json::to_vec(&res).unwrap();
|
||||||
|
stream.write_all(&res).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerError> {
|
||||||
|
shortcuts::exec_shortcut(action);
|
||||||
|
Ok(Response::Empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let rehide_ms = {
|
||||||
|
let config = state.config.read().await;
|
||||||
|
config.rehide_ms
|
||||||
|
};
|
||||||
|
let lease = state.acquire_visibility_lease(rehide_ms).await
|
||||||
|
.map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually?
|
||||||
|
|
||||||
|
let (chan_send, chan_recv) = oneshot::channel();
|
||||||
|
let request_id = state.register_request(chan_send).await;
|
||||||
|
|
||||||
|
// if an error occurs in any of the following, we want to abort the operation
|
||||||
|
// but ? returns immediately, and we want to unregister the request before returning
|
||||||
|
// so we bundle it all up in an async block and return a Result so we can handle errors
|
||||||
|
let proceed = async {
|
||||||
|
let notification = AwsRequestNotification {id: request_id, client, base};
|
||||||
|
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||||
|
|
||||||
|
match chan_recv.await {
|
||||||
|
Ok(Approval::Approved) => {
|
||||||
|
if base {
|
||||||
|
let creds = state.base_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Base(creds)))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let creds = state.session_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Session(creds)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
||||||
|
Err(_e) => Err(HandlerError::Internal),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match proceed.await {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => {
|
||||||
|
state.unregister_request(request_id).await;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lease.release();
|
||||||
|
result
|
||||||
|
}
|
59
src-tauri/src/server/server_unix.rs
Normal file
59
src-tauri/src/server/server_unix.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
|
use tauri::{
|
||||||
|
AppHandle,
|
||||||
|
Manager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
pub type Stream = UnixStream;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
listener: UnixListener,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
|
match std::fs::remove_file("/tmp/creddy.sock") {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = UnixListener::bind("/tmp/creddy.sock")?;
|
||||||
|
let srv = Server { listener, app_handle };
|
||||||
|
rt::spawn(srv.serve());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(self) {
|
||||||
|
loop {
|
||||||
|
self.try_serve()
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error accepting request: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_serve(&self) -> Result<(), HandlerError> {
|
||||||
|
let (stream, _addr) = self.listener.accept().await?;
|
||||||
|
let new_handle = self.app_handle.app_handle();
|
||||||
|
let client_pid = get_client_pid(&stream)?;
|
||||||
|
rt::spawn(async move {
|
||||||
|
super::handle(stream, new_handle, client_pid)
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error responding to request: ");
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> {
|
||||||
|
let cred = stream.peer_cred()?;
|
||||||
|
Ok(cred.pid().unwrap() as u32)
|
||||||
|
}
|
75
src-tauri/src/server/server_win.rs
Normal file
75
src-tauri/src/server/server_win.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use tokio::{
|
||||||
|
net::windows::named_pipe::{
|
||||||
|
NamedPipeServer,
|
||||||
|
ServerOptions,
|
||||||
|
},
|
||||||
|
sync::oneshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use windows::Win32:: {
|
||||||
|
Foundation::HANDLE,
|
||||||
|
System::Pipes::GetNamedPipeClientProcessId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
|
||||||
|
use tauri::async_runtime as rt;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
// used by parent module
|
||||||
|
pub type Stream = NamedPipeServer;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
listener: NamedPipeServer,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
|
let listener = ServerOptions::new()
|
||||||
|
.first_pipe_instance(true)
|
||||||
|
.create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
|
||||||
|
let srv = Server {listener, app_handle};
|
||||||
|
rt::spawn(srv.serve());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(mut self) {
|
||||||
|
loop {
|
||||||
|
if let Err(e) = self.try_serve().await {
|
||||||
|
eprintln!("Error accepting connection: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_serve(&mut self) -> Result<(), HandlerError> {
|
||||||
|
// connect() just waits for a client to connect, it doesn't return anything
|
||||||
|
self.listener.connect().await?;
|
||||||
|
|
||||||
|
// create a new pipe instance to listen for the next client, and swap it in
|
||||||
|
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
let mut stream = std::mem::replace(&mut self.listener, new_listener);
|
||||||
|
let new_handle = self.app_handle.app_handle();
|
||||||
|
let client_pid = get_client_pid(&stream)?;
|
||||||
|
rt::spawn(async move {
|
||||||
|
super::handle(stream, app_handle)
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error responding to request: ");
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_client_pid(pipe: &NamedPipeServer) -> Result<u32, ClientInfoError> {
|
||||||
|
let raw_handle = pipe.as_raw_handle();
|
||||||
|
let mut pid = 0u32;
|
||||||
|
let handle = HANDLE(raw_handle as _);
|
||||||
|
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
||||||
|
pid
|
||||||
|
}
|
60
src-tauri/src/shortcuts.rs
Normal file
60
src-tauri/src/shortcuts.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use tauri::{
|
||||||
|
GlobalShortcutManager,
|
||||||
|
Manager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::APP;
|
||||||
|
use crate::config::HotkeysConfig;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::terminal;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ShortcutAction {
|
||||||
|
ShowWindow,
|
||||||
|
LaunchTerminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn exec_shortcut(action: ShortcutAction) {
|
||||||
|
match action {
|
||||||
|
ShortcutAction::ShowWindow => {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
app.get_window("main")
|
||||||
|
.ok_or("Couldn't find application main window")
|
||||||
|
.map(|w| w.show().error_popup("Failed to show window"))
|
||||||
|
.error_popup("Failed to show window");
|
||||||
|
},
|
||||||
|
ShortcutAction::LaunchTerminal => {
|
||||||
|
rt::spawn(async {
|
||||||
|
terminal::launch(false).await.error_popup("Failed to launch terminal");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
let mut manager = app.global_shortcut_manager();
|
||||||
|
manager.unregister_all()?;
|
||||||
|
|
||||||
|
if hotkeys.show_window.enabled {
|
||||||
|
manager.register(
|
||||||
|
&hotkeys.show_window.keys,
|
||||||
|
|| exec_shortcut(ShortcutAction::ShowWindow)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hotkeys.launch_terminal.enabled {
|
||||||
|
manager.register(
|
||||||
|
&hotkeys.launch_terminal.keys,
|
||||||
|
|| exec_shortcut(ShortcutAction::LaunchTerminal)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::oneshot::Sender,
|
|
||||||
sync::RwLock,
|
sync::RwLock,
|
||||||
time::sleep,
|
sync::oneshot::{self, Sender},
|
||||||
};
|
};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tauri::async_runtime as runtime;
|
use tauri::{
|
||||||
use tauri::Manager;
|
Manager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::APP;
|
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
Session,
|
Session,
|
||||||
BaseCredentials,
|
BaseCredentials,
|
||||||
@ -18,9 +18,74 @@ use crate::credentials::{
|
|||||||
};
|
};
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::ipc::{self, Approval};
|
use crate::ipc::{self, Approval};
|
||||||
use crate::clientinfo::Client;
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::server::Server;
|
use crate::shortcuts;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Visibility {
|
||||||
|
leases: usize,
|
||||||
|
original: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visibility {
|
||||||
|
fn new() -> Self {
|
||||||
|
Visibility { leases: 0, original: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acquire(&mut self, delay_ms: u64) -> Result<VisibilityLease, WindowError> {
|
||||||
|
let app = crate::app::APP.get().unwrap();
|
||||||
|
let window = app.get_window("main")
|
||||||
|
.ok_or(WindowError::NoMainWindow)?;
|
||||||
|
|
||||||
|
self.leases += 1;
|
||||||
|
if self.original.is_none() {
|
||||||
|
let is_visible = window.is_visible()?;
|
||||||
|
self.original = Some(is_visible);
|
||||||
|
if !is_visible {
|
||||||
|
window.show()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.set_focus()?;
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let lease = VisibilityLease { notify: tx };
|
||||||
|
|
||||||
|
let delay = Duration::from_millis(delay_ms);
|
||||||
|
let handle = app.app_handle();
|
||||||
|
rt::spawn(async move {
|
||||||
|
// We don't care if it's an error; lease being dropped should be handled identically
|
||||||
|
let _ = rx.await;
|
||||||
|
tokio::time::sleep(delay).await;
|
||||||
|
// we can't use `self` here because we would have to move it into the async block
|
||||||
|
let state = handle.state::<AppState>();
|
||||||
|
let mut visibility = state.visibility.write().await;
|
||||||
|
visibility.leases -= 1;
|
||||||
|
if visibility.leases == 0 {
|
||||||
|
if let Some(false) = visibility.original {
|
||||||
|
window.hide().error_print();
|
||||||
|
}
|
||||||
|
visibility.original = None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(lease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VisibilityLease {
|
||||||
|
notify: Sender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisibilityLease {
|
||||||
|
pub fn release(self) {
|
||||||
|
rt::spawn(async move {
|
||||||
|
if let Err(_) = self.notify.send(()) {
|
||||||
|
eprintln!("Error releasing visibility lease")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -28,22 +93,30 @@ pub struct AppState {
|
|||||||
pub config: RwLock<AppConfig>,
|
pub config: RwLock<AppConfig>,
|
||||||
pub session: RwLock<Session>,
|
pub session: RwLock<Session>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
|
||||||
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
pub pending_terminal_request: RwLock<bool>,
|
||||||
server: RwLock<Server>,
|
// setup_errors is never modified and so doesn't need to be wrapped in RwLock
|
||||||
|
pub setup_errors: Vec<String>,
|
||||||
pool: sqlx::SqlitePool,
|
pool: sqlx::SqlitePool,
|
||||||
|
visibility: RwLock<Visibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(config: AppConfig, session: Session, server: Server, pool: SqlitePool) -> AppState {
|
pub fn new(
|
||||||
|
config: AppConfig,
|
||||||
|
session: Session,
|
||||||
|
pool: SqlitePool,
|
||||||
|
setup_errors: Vec<String>,
|
||||||
|
) -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
session: RwLock::new(session),
|
session: RwLock::new(session),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
waiting_requests: RwLock::new(HashMap::new()),
|
||||||
bans: RwLock::new(HashSet::new()),
|
pending_terminal_request: RwLock::new(false),
|
||||||
server: RwLock::new(server),
|
setup_errors,
|
||||||
pool,
|
pool,
|
||||||
|
visibility: RwLock::new(Visibility::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,14 +132,16 @@ impl AppState {
|
|||||||
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
||||||
let mut live_config = self.config.write().await;
|
let mut live_config = self.config.write().await;
|
||||||
|
|
||||||
|
// update autostart if necessary
|
||||||
if new_config.start_on_login != live_config.start_on_login {
|
if new_config.start_on_login != live_config.start_on_login {
|
||||||
config::set_auto_launch(new_config.start_on_login)?;
|
config::set_auto_launch(new_config.start_on_login)?;
|
||||||
}
|
}
|
||||||
if new_config.listen_addr != live_config.listen_addr
|
|
||||||
|| new_config.listen_port != live_config.listen_port
|
// re-register hotkeys if necessary
|
||||||
|
if new_config.hotkeys.show_window != live_config.hotkeys.show_window
|
||||||
|
|| new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal
|
||||||
{
|
{
|
||||||
let mut sv = self.server.write().await;
|
shortcuts::register_hotkeys(&new_config.hotkeys)?;
|
||||||
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new_config.save(&self.pool).await?;
|
new_config.save(&self.pool).await?;
|
||||||
@ -74,26 +149,26 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
|
pub async fn register_request(&self, sender: Sender<Approval>) -> u64 {
|
||||||
let count = {
|
let count = {
|
||||||
let mut c = self.request_count.write().await;
|
let mut c = self.request_count.write().await;
|
||||||
*c += 1;
|
*c += 1;
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.insert(*count, chan); // `count` is the request id
|
waiting_requests.insert(*count, sender); // `count` is the request id
|
||||||
*count
|
*count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_request(&self, id: u64) {
|
pub async fn unregister_request(&self, id: u64) {
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.remove(&id);
|
waiting_requests.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn req_count(&self) -> usize {
|
pub async fn acquire_visibility_lease(&self, delay: u64) -> Result<VisibilityLease, WindowError> {
|
||||||
let open_requests = self.open_requests.read().await;
|
let mut visibility = self.visibility.write().await;
|
||||||
open_requests.len()
|
visibility.acquire(delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
||||||
@ -102,31 +177,12 @@ impl AppState {
|
|||||||
session.renew_if_expired().await?;
|
session.renew_if_expired().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
let chan = open_requests
|
waiting_requests
|
||||||
.remove(&response.id)
|
.remove(&response.id)
|
||||||
.ok_or(SendResponseError::NotFound)
|
.ok_or(SendResponseError::NotFound)?
|
||||||
?;
|
.send(response.approval)
|
||||||
|
.map_err(|_| SendResponseError::Abandoned)
|
||||||
chan.send(response.approval)
|
|
||||||
.map_err(|_e| SendResponseError::Abandoned)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_ban(&self, client: Option<Client>) {
|
|
||||||
let mut bans = self.bans.write().await;
|
|
||||||
bans.insert(client.clone());
|
|
||||||
|
|
||||||
runtime::spawn(async move {
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
let app = APP.get().unwrap();
|
|
||||||
let state = app.state::<AppState>();
|
|
||||||
let mut bans = state.bans.write().await;
|
|
||||||
bans.remove(&client);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_banned(&self, client: &Option<Client>) -> bool {
|
|
||||||
self.bans.read().await.contains(&client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
|
pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
|
||||||
@ -141,22 +197,21 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn is_unlocked(&self) -> bool {
|
||||||
let session = self.session.read().await;
|
let session = self.session.read().await;
|
||||||
match *session {
|
matches!(*session, Session::Unlocked{..})
|
||||||
Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
|
|
||||||
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
|
||||||
Session::Empty => Err(GetCredentialsError::Empty),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn base_creds_cloned(&self) -> Result<BaseCredentials, GetCredentialsError> {
|
||||||
let session = self.session.read().await;
|
let app_session = self.session.read().await;
|
||||||
match *session {
|
let (base, _session) = app_session.try_get()?;
|
||||||
Session::Unlocked{ref session, ..} => Ok(serde_json::to_string(session).unwrap()),
|
Ok(base.clone())
|
||||||
Session::Locked(_) => Err(GetCredentialsError::Locked),
|
}
|
||||||
Session::Empty => Err(GetCredentialsError::Empty),
|
|
||||||
}
|
pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
|
||||||
|
let app_session = self.session.read().await;
|
||||||
|
let (_bsae, session) = app_session.try_get()?;
|
||||||
|
Ok(session.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
|
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
|
||||||
@ -165,4 +220,21 @@ impl AppState {
|
|||||||
*app_session = Session::Unlocked {base, session};
|
*app_session = Session::Unlocked {base, session};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
if *req {
|
||||||
|
// if a request is already pending, we can't register a new one
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*req = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister_terminal_request(&self) {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
*req = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
78
src-tauri/src/terminal.rs
Normal file
78
src-tauri/src/terminal.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use crate::app::APP;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
|
let app = APP.get().unwrap();
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
|
// register_terminal_request() returns Err if there is another request pending
|
||||||
|
if state.register_terminal_request().await.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = {
|
||||||
|
let config = state.config.read().await;
|
||||||
|
let mut cmd = Command::new(&config.terminal.exec);
|
||||||
|
cmd.args(&config.terminal.args);
|
||||||
|
cmd
|
||||||
|
};
|
||||||
|
|
||||||
|
// if session is unlocked or empty, wait for credentials from frontend
|
||||||
|
if !state.is_unlocked().await {
|
||||||
|
app.emit_all("launch-terminal-request", ())?;
|
||||||
|
let lease = state.acquire_visibility_lease(0).await
|
||||||
|
.map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually?
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
app.once_global("credentials-event", move |e| {
|
||||||
|
let success = match e.payload() {
|
||||||
|
Some("\"unlocked\"") | Some("\"entered\"") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let _ = tx.send(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !rx.await.unwrap_or(false) {
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
return Ok(()); // request was canceled by user
|
||||||
|
}
|
||||||
|
lease.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// more lock-management
|
||||||
|
{
|
||||||
|
let app_session = state.session.read().await;
|
||||||
|
// session should really be unlocked at this point, but if the frontend misbehaves
|
||||||
|
// (i.e. lies about unlocking) we could end up here with a locked session
|
||||||
|
// this will result in an error popup to the user (see main hotkey handler)
|
||||||
|
let (base_creds, session_creds) = app_session.try_get()?;
|
||||||
|
if use_base {
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
|
||||||
|
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
|
||||||
|
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = match cmd.spawn() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
||||||
|
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
||||||
|
},
|
||||||
|
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
|
||||||
|
res?; // ? auto-conversion is more liberal than .into()
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -8,11 +8,12 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.2.1"
|
"version": "0.3.4"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"os": {"all": true}
|
"os": {"all": true},
|
||||||
|
"dialog": {"open": true}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@ -44,7 +45,11 @@
|
|||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": ""
|
"timestampUrl": "",
|
||||||
|
"wix": {
|
||||||
|
"fragmentPaths": ["conf/cli.wxs"],
|
||||||
|
"componentRefs": ["CliBinary", "AddToPath"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
|
@ -16,6 +16,25 @@ listen('credentials-request', (tauriEvent) => {
|
|||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen('launch-terminal-request', async (tauriEvent) => {
|
||||||
|
if ($appState.currentRequest === null) {
|
||||||
|
let status = await invoke('get_session_status');
|
||||||
|
if (status === 'locked') {
|
||||||
|
navigate('Unlock');
|
||||||
|
}
|
||||||
|
else if (status === 'empty') {
|
||||||
|
navigate('EnterCredentials');
|
||||||
|
}
|
||||||
|
// else, session is unlocked, so do nothing
|
||||||
|
// (although we shouldn't even get the event in that case)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
invoke('get_setup_errors')
|
||||||
|
.then(errs => {
|
||||||
|
$appState.setupErrors = errs.map(e => ({msg: e, show: true}));
|
||||||
|
});
|
||||||
|
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ export default function() {
|
|||||||
|
|
||||||
resolvers: [],
|
resolvers: [],
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.items.length;
|
||||||
|
},
|
||||||
|
|
||||||
put(item) {
|
put(item) {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
let resolver = this.resolvers.shift();
|
let resolver = this.resolvers.shift();
|
||||||
|
@ -8,6 +8,7 @@ export let appState = writable({
|
|||||||
currentRequest: null,
|
currentRequest: null,
|
||||||
pendingRequests: queue(),
|
pendingRequests: queue(),
|
||||||
credentialStatus: 'locked',
|
credentialStatus: 'locked',
|
||||||
|
setupErrors: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
13
src/ui/KeyCombo.svelte
Normal file
13
src/ui/KeyCombo.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let keys;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-x-[0.2em] items-center">
|
||||||
|
{#each keys as key, i}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="mt-[-0.1em]">+</span>
|
||||||
|
{/if}
|
||||||
|
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
||||||
|
{/each}
|
||||||
|
</div>
|
@ -1,113 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
export let color = 'base-content';
|
export let thickness = 8;
|
||||||
export let thickness = '2px';
|
|
||||||
let classes = '';
|
let classes = '';
|
||||||
export { classes as class };
|
export { classes as class };
|
||||||
|
|
||||||
const colorVars = {
|
const radius = (100 - thickness) / 2;
|
||||||
'primary': 'p',
|
// the px are fake, but we need them to satisfy css calc()
|
||||||
'primary-focus': 'pf',
|
const circumference = `${2 * Math.PI * radius}px`;
|
||||||
'primary-content': 'pc',
|
|
||||||
'secondary': 's',
|
|
||||||
'secondary-focus': 'sf',
|
|
||||||
'secondary-content': 'sc',
|
|
||||||
'accent': 'a',
|
|
||||||
'accent-focus': 'af',
|
|
||||||
'accent-content': 'ac',
|
|
||||||
'neutral': 'n',
|
|
||||||
'neutral-focus': 'nf',
|
|
||||||
'neutral-content': 'nc',
|
|
||||||
'base-100': 'b1',
|
|
||||||
'base-200': 'b2',
|
|
||||||
'base-300': 'b3',
|
|
||||||
'base-content': 'bc',
|
|
||||||
'info': 'in',
|
|
||||||
'info-content': 'inc',
|
|
||||||
'success': 'su',
|
|
||||||
'success-content': 'suc',
|
|
||||||
'warning': 'wa',
|
|
||||||
'warning-content': 'wac',
|
|
||||||
'error': 'er',
|
|
||||||
'error-content': 'erc',
|
|
||||||
}
|
|
||||||
|
|
||||||
let arcStyle = `border-width: ${thickness};`;
|
|
||||||
arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#spinner {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
animation: spin;
|
<svg
|
||||||
animation-duration: 1.5s;
|
style:--circumference={circumference}
|
||||||
animation-iteration-count: infinite;
|
class={classes}
|
||||||
animation-timing-function: linear;
|
viewBox="0 0 100 100"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<circle cx="50" cy="50" r={radius} stroke-width={thickness} />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
circle {
|
||||||
|
fill: transparent;
|
||||||
|
stroke-dasharray: var(--circumference);
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
animation: chase 3s infinite,
|
||||||
|
spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chase {
|
||||||
|
0% { stroke-dashoffset: calc(-1 * var(--circumference)); }
|
||||||
|
50% { stroke-dashoffset: calc(-2 * var(--circumference)); }
|
||||||
|
100% { stroke-dashoffset: calc(-3 * var(--circumference)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
50% { transform: rotate(225deg); }
|
50% { transform: rotate(135deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(270deg); }
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.arc {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-top {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-right {
|
|
||||||
animation: spin-right;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-bottom {
|
|
||||||
animation: spin-bottom;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-left {
|
|
||||||
animation: spin-left;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-top {
|
|
||||||
0% { transform: rotate(-45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(-45deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-right {
|
|
||||||
0% { transform: rotate(45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(405deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-bottom {
|
|
||||||
0% { transform: rotate(135deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(495deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-left {
|
|
||||||
0% { transform: rotate(225deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(585deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="spinner" class="w-6 h-6 {classes}">
|
|
||||||
<div class="arc arc-top w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-right w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-left w-full h-full" style={arcStyle}></div>
|
|
||||||
</div>
|
|
27
src/ui/settings/FileSetting.svelte
Normal file
27
src/ui/settings/FileSetting.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<Setting {title}>
|
||||||
|
<div slot="input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-sm input-bordered grow text-right"
|
||||||
|
bind:value
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={async () => value = await open()}
|
||||||
|
>Browse</button>
|
||||||
|
</div>
|
||||||
|
<slot name="description" slot="description"></slot>
|
||||||
|
</Setting>
|
72
src/ui/settings/Keybind.svelte
Normal file
72
src/ui/settings/Keybind.svelte
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import KeyCombo from '../KeyCombo.svelte';
|
||||||
|
|
||||||
|
export let description;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const id = Math.random().toString().slice(2);
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const MODIFIERS = new Set(['Alt', 'AltGraph', 'Control', 'Fn', 'FnLock', 'Meta', 'Shift', 'Super', ]);
|
||||||
|
|
||||||
|
|
||||||
|
let listening = false;
|
||||||
|
let keysPressed = [];
|
||||||
|
|
||||||
|
function addModifiers(event) {
|
||||||
|
// add modifier key if it isn't already present
|
||||||
|
if (MODIFIERS.has(event.key) && keysPressed.indexOf(event.key) === -1) {
|
||||||
|
keysPressed.push(event.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMainKey(event) {
|
||||||
|
if (!MODIFIERS.has(event.key)) {
|
||||||
|
keysPressed.push(event.key);
|
||||||
|
|
||||||
|
value.keys = keysPressed.join('+');
|
||||||
|
dispatch('update', {value});
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
// don't re-listen if we already are
|
||||||
|
if (listening) return;
|
||||||
|
|
||||||
|
listening = true;
|
||||||
|
window.addEventListener('keydown', addModifiers);
|
||||||
|
window.addEventListener('keyup', addMainKey);
|
||||||
|
// setTimeout avoids reacting to the click event that we are currently processing
|
||||||
|
setTimeout(() => window.addEventListener('click', unlisten), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlisten() {
|
||||||
|
listening = false;
|
||||||
|
keysPressed = [];
|
||||||
|
window.removeEventListener('keydown', addModifiers);
|
||||||
|
window.removeEventListener('keyup', addMainKey);
|
||||||
|
window.removeEventListener('click', unlisten);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
bind:checked={value.enabled}
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
<label for={id} class="cursor-pointer ml-4 text-lg">{description}</label>
|
||||||
|
|
||||||
|
<button class="h-12 p-2 rounded border border-neutral cursor-pointer text-center" on:click={listen}>
|
||||||
|
{#if listening}
|
||||||
|
Click to cancel
|
||||||
|
{:else}
|
||||||
|
<KeyCombo keys={value.keys.split('+')} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
@ -5,19 +5,26 @@
|
|||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
export let unit = '';
|
export let unit = '';
|
||||||
export let min = null;
|
export let min = null;
|
||||||
export let max = null;
|
export let max = null;
|
||||||
export let decimal = false;
|
export let decimal = false;
|
||||||
|
export let debounceInterval = 0;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
$: localValue = value.toString();
|
$: localValue = value.toString();
|
||||||
let lastInputTime = null;
|
let lastInputTime = null;
|
||||||
function debounce(event) {
|
function debounce(event) {
|
||||||
lastInputTime = Date.now();
|
|
||||||
localValue = localValue.replace(/[^-0-9.]/g, '');
|
localValue = localValue.replace(/[^-0-9.]/g, '');
|
||||||
|
|
||||||
|
if (debounceInterval === 0) {
|
||||||
|
updateValue(localValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastInputTime = Date.now();
|
||||||
const eventTime = lastInputTime;
|
const eventTime = lastInputTime;
|
||||||
const pendingValue = localValue;
|
const pendingValue = localValue;
|
||||||
window.setTimeout(
|
window.setTimeout(
|
||||||
@ -27,7 +34,7 @@
|
|||||||
updateValue(pendingValue);
|
updateValue(pendingValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
500
|
debounceInterval,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,14 +6,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div>
|
||||||
<div class="flex justify-between">
|
<div class="flex flex-wrap justify-between gap-y-4">
|
||||||
<h3 class="text-lg font-bold">{title}</h3>
|
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
||||||
<slot name="input"></slot>
|
{#if $$slots.input}
|
||||||
</div>
|
<slot name="input"></slot>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if $$slots.description}
|
{#if $$slots.description}
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
<slot name="description"></slot>
|
<slot name="description"></slot>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
14
src/ui/settings/SettingsGroup.svelte
Normal file
14
src/ui/settings/SettingsGroup.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
export let name;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="divider mt-0 mb-8">
|
||||||
|
<h2 class="text-xl font-bold">{name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-12">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
22
src/ui/settings/TextSetting.svelte
Normal file
22
src/ui/settings/TextSetting.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<Setting {title}>
|
||||||
|
<div slot="input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-sm input-bordered grow text-right"
|
||||||
|
bind:value
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<slot name="description" slot="description"></slot>
|
||||||
|
</Setting>
|
@ -1,3 +1,5 @@
|
|||||||
export { default as Setting } from './Setting.svelte';
|
export { default as Setting } from './Setting.svelte';
|
||||||
export { default as ToggleSetting } from './ToggleSetting.svelte';
|
export { default as ToggleSetting } from './ToggleSetting.svelte';
|
||||||
export { default as NumericSetting } from './NumericSetting.svelte';
|
export { default as NumericSetting } from './NumericSetting.svelte';
|
||||||
|
export { default as FileSetting } from './FileSetting.svelte';
|
||||||
|
export { default as TextSetting } from './TextSetting.svelte';
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState, completeRequest } from '../lib/state.js';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
import KeyCombo from '../ui/KeyCombo.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Send response to backend, display error if applicable
|
// Send response to backend, display error if applicable
|
||||||
@ -46,16 +47,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract executable name from full path
|
// Extract executable name from full path
|
||||||
let appName = null;
|
const client = $appState.currentRequest.client;
|
||||||
if ($appState.currentRequest.clients.length === 1) {
|
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
||||||
let path = $appState.currentRequest.clients[0].exe;
|
const appName = m[1] || m[2];
|
||||||
let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
|
|
||||||
appName = m[1] || m[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executable paths can be long, so ensure they only break on \ or /
|
// Executable paths can be long, so ensure they only break on \ or /
|
||||||
function breakPath(client) {
|
function breakPath(path) {
|
||||||
return client.exe.replace(/(\\|\/)/g, '$1<wbr>');
|
return path.replace(/(\\|\/)/g, '$1<wbr>');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the request has already been approved/denied, send response immediately
|
// if the request has already been approved/denied, send response immediately
|
||||||
@ -96,29 +94,25 @@
|
|||||||
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
||||||
{#each $appState.currentRequest.clients as client}
|
<div class="text-right">Path:</div>
|
||||||
<div class="text-right">Path:</div>
|
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
|
||||||
<code class="">{@html client ? breakPath(client) : 'Unknown'}</code>
|
<div class="text-right">PID:</div>
|
||||||
<div class="text-right">PID:</div>
|
<code>{client.pid}</code>
|
||||||
<code>{client ? client.pid : 'Unknown'}</code>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<Link target={deny} hotkey="Escape">
|
<Link target={deny} hotkey="Escape">
|
||||||
<button class="btn btn-error justify-self-start">
|
<button class="btn btn-error justify-self-start">
|
||||||
Deny
|
<span class="mr-2">Deny</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
|
<KeyCombo keys={['Esc']} />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
<Link target={approve} hotkey="Enter" shift="{true}">
|
||||||
<button class="btn btn-success justify-self-end">
|
<button class="btn btn-success justify-self-end">
|
||||||
Approve
|
<span class="mr-2">Approve</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
|
<KeyCombo keys={['Shift', 'Enter']} />
|
||||||
<span class="mx-0.5">+</span>
|
|
||||||
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">Enter</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { getRootCause } from '../lib/errors.js';
|
import { getRootCause } from '../lib/errors.js';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
@ -31,6 +32,7 @@
|
|||||||
try {
|
try {
|
||||||
saving = true;
|
saving = true;
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
await invoke('save_credentials', {credentials, passphrase});
|
||||||
|
emit('credentials-event', 'entered');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -39,14 +41,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e.code === "GetSession") {
|
window.error = e;
|
||||||
let root = getRootCause(e);
|
const root = getRootCause(e);
|
||||||
|
if (e.code === 'GetSession' && root.code) {
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errorMsg = e.msg;
|
errorMsg = e.msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the alert already existed, shake it
|
||||||
if (alert) {
|
if (alert) {
|
||||||
alert.shake();
|
alert.shake();
|
||||||
}
|
}
|
||||||
@ -54,6 +58,11 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'enter-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -71,13 +80,13 @@
|
|||||||
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving }
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
@ -10,13 +10,11 @@
|
|||||||
|
|
||||||
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
import vaultDoorSvg from '../assets/vault_door.svg?raw';
|
||||||
|
|
||||||
|
let launchBase = false;
|
||||||
// onMount(async () => {
|
function launchTerminal() {
|
||||||
// // will block until a request comes in
|
invoke('launch_terminal', {base: launchBase});
|
||||||
// let req = await $appState.pendingRequests.get();
|
launchBase = false;
|
||||||
// $appState.currentRequest = req;
|
}
|
||||||
// navigate('Approve');
|
|
||||||
// });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -25,25 +23,45 @@
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
|
||||||
{#await invoke('get_session_status') then status}
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{#if status === 'locked'}
|
{@html vaultDoorSvg}
|
||||||
|
{#await invoke('get_session_status') then status}
|
||||||
|
{#if status === 'locked'}
|
||||||
|
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
||||||
<h2 class="text-2xl font-bold">Creddy is locked</h2>
|
<Link target="Unlock" hotkey="Enter" class="w-64">
|
||||||
<Link target="Unlock" hotkey="Enter" class="w-64">
|
<button class="btn btn-primary w-full">Unlock</button>
|
||||||
<button class="btn btn-primary w-full">Unlock</button>
|
</Link>
|
||||||
</Link>
|
|
||||||
|
|
||||||
{:else if status === 'unlocked'}
|
{:else if status === 'unlocked'}
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
||||||
<h2 class="text-2xl font-bold">Waiting for requests</h2>
|
<button class="btn btn-primary w-full" on:click={launchTerminal}>
|
||||||
|
Launch Terminal
|
||||||
|
</button>
|
||||||
|
<label class="label cursor-pointer flex items-center space-x-2">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
||||||
|
<span class="label-text">Launch with base credentials</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
{:else if status === 'empty'}
|
{:else if status === 'empty'}
|
||||||
{@html vaultDoorSvg}
|
<h2 class="text-2xl font-bold">No credentials found</h2>
|
||||||
<h2 class="text-2xl font-bold">No credentials found</h2>
|
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
||||||
<Link target="EnterCredentials" hotkey="Enter" class="w-64">
|
<button class="btn btn-primary w-full">Enter Credentials</button>
|
||||||
<button class="btn btn-primary w-full">Enter Credentials</button>
|
</Link>
|
||||||
</Link>
|
{/if}
|
||||||
{/if}
|
{/await}
|
||||||
{/await}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $appState.setupErrors.some(e => e.show)}
|
||||||
|
<div class="toast">
|
||||||
|
{#each $appState.setupErrors as error}
|
||||||
|
{#if error.show}
|
||||||
|
<div class="alert alert-error shadow-lg">
|
||||||
|
{error.msg}
|
||||||
|
<button class="btn btn-sm btn-alert-error" on:click={() => error.show = false}>Ok</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -6,49 +6,53 @@
|
|||||||
import Nav from '../ui/Nav.svelte';
|
import Nav from '../ui/Nav.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
|
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
||||||
|
import Keybind from '../ui/settings/Keybind.svelte';
|
||||||
|
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting } from '../ui/settings';
|
||||||
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { backInOut } from 'svelte/easing';
|
import { backInOut } from 'svelte/easing';
|
||||||
|
|
||||||
|
|
||||||
|
// make an independent copy so it can differ from the main config object
|
||||||
|
let config = JSON.parse(JSON.stringify($appState.config));
|
||||||
|
$: configModified = JSON.stringify(config) !== JSON.stringify($appState.config);
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
await invoke('save_config', {config: $appState.config});
|
await invoke('save_config', {config});
|
||||||
|
$appState.config = await invoke('get_config');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
$appState.config = await invoke('get_config');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let osType = '';
|
let osType = null;
|
||||||
type().then(t => osType = t);
|
type().then(t => osType = t);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Nav>
|
<Nav>
|
||||||
<h2 slot="title" class="text-2xl font-bold">Settings</h2>
|
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
{#await invoke('get_config') then config}
|
<div class="max-w-lg mx-auto mt-1.5 mb-24 p-4 space-y-16">
|
||||||
<div class="max-w-md mx-auto mt-1.5 p-4">
|
<SettingsGroup name="General">
|
||||||
<!-- <h2 class="text-2xl font-bold text-center">Settings</h2> -->
|
<ToggleSetting title="Start on login" bind:value={config.start_on_login}>
|
||||||
|
|
||||||
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Start Creddy when you log in to your computer.
|
Start Creddy when you log in to your computer.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ToggleSetting>
|
</ToggleSetting>
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Minimize to the system tray at startup.
|
Minimize to the system tray at startup.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ToggleSetting>
|
</ToggleSetting>
|
||||||
|
|
||||||
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
How long to wait after a request is approved/denied before minimizing
|
How long to wait after a request is approved/denied before minimizing
|
||||||
the window to tray. Only applicable if the window was minimized
|
the window to tray. Only applicable if the window was minimized
|
||||||
@ -56,18 +60,6 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</NumericSetting>
|
</NumericSetting>
|
||||||
|
|
||||||
<NumericSetting
|
|
||||||
title="Listen port"
|
|
||||||
bind:value={$appState.config.listen_port}
|
|
||||||
min={osType === 'Windows_NT' ? 1 : 0}
|
|
||||||
on:update={save}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Listen for credentials requests on this port.
|
|
||||||
(Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
|
|
||||||
</svelte:fragment>
|
|
||||||
</NumericSetting>
|
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<Setting title="Update credentials">
|
||||||
<Link slot="input" target="EnterCredentials">
|
<Link slot="input" target="EnterCredentials">
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
<button class="btn btn-sm btn-primary">Update</button>
|
||||||
@ -76,8 +68,30 @@
|
|||||||
Update or re-enter your encrypted credentials.
|
Update or re-enter your encrypted credentials.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Setting>
|
</Setting>
|
||||||
</div>
|
|
||||||
{/await}
|
<FileSetting
|
||||||
|
title="Terminal emulator"
|
||||||
|
bind:value={config.terminal.exec}
|
||||||
|
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
||||||
|
</svelte:fragment>
|
||||||
|
</FileSetting>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
|
<SettingsGroup name="Hotkeys">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
||||||
|
<Keybind description="Show Creddy" bind:value={config.hotkeys.show_window} />
|
||||||
|
<Keybind description="Launch terminal" bind:value={config.hotkeys.launch_terminal} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
||||||
@ -91,4 +105,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if configModified}
|
||||||
|
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
||||||
|
<div class="alert shadow-lg no-animation">
|
||||||
|
<span>You have unsaved changes.</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- <button class="btn btn-sm btn-ghost">Cancel</button> -->
|
||||||
|
<buton class="btn btn-sm btn-primary" on:click={save}>Save</buton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
@ -26,6 +27,7 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
let r = await invoke('unlock', {passphrase});
|
let r = await invoke('unlock', {passphrase});
|
||||||
$appState.credentialStatus = 'unlocked';
|
$appState.credentialStatus = 'unlocked';
|
||||||
|
emit('credentials-event', 'unlocked');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -34,23 +36,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
window.error = e;
|
const root = getRootCause(e);
|
||||||
if (e.code === 'GetSession') {
|
if (e.code === 'GetSession' && root.code) {
|
||||||
let root = getRootCause(e);
|
|
||||||
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errorMsg = e.msg;
|
errorMsg = e.msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the alert already existed, shake it
|
||||||
if (alert) {
|
if (alert) {
|
||||||
alert.shake();
|
alert.shake();
|
||||||
}
|
}
|
||||||
|
|
||||||
saving = true;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'unlock-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTime = Date.now();
|
loadTime = Date.now();
|
||||||
})
|
})
|
||||||
@ -69,13 +76,13 @@
|
|||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving}
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
Reference in New Issue
Block a user