Compare commits

..

2 Commits

19 changed files with 515 additions and 519 deletions

View File

@ -16,3 +16,4 @@
* Generalize Request across both credentials and terminal launch? * Generalize Request across both credentials and terminal launch?
* Make hotkey configuration a little more tolerant of slight mistiming * 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) * 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

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "creddy", "name": "creddy",
"version": "0.3.1", "version": "0.3.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "creddy", "name": "creddy",
"version": "0.3.1", "version": "0.3.3",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.0.2", "@tauri-apps/api": "^1.0.2",
"daisyui": "^2.51.5" "daisyui": "^2.51.5"

48
src-tauri/Cargo.lock generated
View File

@ -1035,7 +1035,7 @@ dependencies = [
[[package]] [[package]]
name = "creddy" name = "creddy"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"argon2", "argon2",
"auto-launch", "auto-launch",
@ -1047,7 +1047,6 @@ dependencies = [
"clap", "clap",
"dirs 5.0.1", "dirs 5.0.1",
"is-terminal", "is-terminal",
"netstat2",
"once_cell", "once_cell",
"serde", "serde",
"serde_json", "serde_json",
@ -1062,6 +1061,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"which", "which",
"windows 0.51.1",
] ]
[[package]] [[package]]
@ -2641,20 +2641,6 @@ dependencies = [
"jni-sys", "jni-sys",
] ]
[[package]]
name = "netstat2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0faa3f4ad230fd2bf2a5dad71476ecbaeaed904b3c7e7e5b1f266c415c03761f"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"libc",
"num-derive",
"num-traits",
"thiserror",
]
[[package]] [[package]]
name = "new_debug_unreachable" name = "new_debug_unreachable"
version = "1.0.4" version = "1.0.4"
@ -2708,17 +2694,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -5261,6 +5236,16 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "windows"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
dependencies = [
"windows-core",
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-bindgen" name = "windows-bindgen"
version = "0.39.0" version = "0.39.0"
@ -5271,6 +5256,15 @@ dependencies = [
"windows-tokens", "windows-tokens",
] ]
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.39.0" version = "0.39.0"

View File

@ -30,7 +30,6 @@ tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-wo
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"
@ -47,6 +46,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" 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

View File

@ -93,7 +93,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
}; };
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 let Err(_e) = config::set_auto_launch(conf.start_on_login) { if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
@ -110,7 +110,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
.show()?; .show()?;
} }
let state = AppState::new(conf, session, srv, pool, setup_errors); let state = AppState::new(conf, session, pool, setup_errors);
app.manage(state); app.manage(state);
Ok(()) Ok(())
} }

View File

@ -19,13 +19,14 @@ fn main() {
let res = match args.subcommand() { let res = match args.subcommand() {
None | Some(("run", _)) => launch_gui(), None | Some(("run", _)) => launch_gui(),
Some(("show", m)) => cli::show(m), Some(("get", m)) => cli::get(m),
Some(("exec", m)) => cli::exec(m), Some(("exec", m)) => cli::exec(m),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Err(e) = res { if let Err(e) = res {
eprintln!("Error: {e}"); eprintln!("Error: {e}");
process::exit(1);
} }
} }

View File

@ -1,7 +1,6 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::process::Command as ChildCommand; use std::process::Command as ChildCommand;
#[cfg(unix)] use std::time::Duration;
use std::os::unix::process::CommandExt;
use clap::{ use clap::{
Command, Command,
@ -9,16 +8,24 @@ use clap::{
ArgMatches, ArgMatches,
ArgAction ArgAction
}; };
use tokio::{ use tokio::io::{AsyncReadExt, AsyncWriteExt};
net::TcpStream,
io::{AsyncReadExt, AsyncWriteExt}, use crate::credentials::Credentials;
use crate::errors::*;
use crate::server::{Request, Response};
#[cfg(unix)]
use {
std::os::unix::process::CommandExt,
std::path::Path,
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> {
@ -30,8 +37,8 @@ pub fn parser() -> Command<'static> {
.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')
@ -59,10 +66,13 @@ pub fn parser() -> Command<'static> {
} }
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(())
} }
@ -76,18 +86,16 @@ 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)]
@ -122,40 +130,44 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
#[tokio::main] #[tokio::main]
async fn get_credentials(base: bool) -> Result<String, RequestError> { async fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
let pool = app::connect_db().await?; let req = Request::GetAwsCredentials { base };
let config = AppConfig::load(&pool).await?; let mut data = serde_json::to_string(&req).unwrap();
let path = if base {"/creddy/base-credentials"} else {"/"}; // server expects newline marking end of request
data.push('\n');
let mut stream = TcpStream::connect((config.listen_addr, config.listen_port)).await?; let mut stream = connect().await?;
let req = format!("GET {path} HTTP/1.0\r\n\r\n"); stream.write_all(&data.as_bytes()).await?;
stream.write_all(req.as_bytes()).await?;
// some day we'll have a proper HTTP parser let mut buf = Vec::with_capacity(1024);
let mut buf = vec![0; 8192];
stream.read_to_end(&mut buf).await?; stream.read_to_end(&mut buf).await?;
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
let status = buf.split(|&c| &[c] == b" ") match res {
.skip(1) Ok(Response::Aws(creds)) => Ok(creds),
.next() // Eventually we will want this
.ok_or(RequestError::MalformedHttpResponse)?; // Ok(r) => Err(RequestError::Unexpected(r)),
Err(e) => Err(RequestError::Server(e)),
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)?; #[cfg(windows)]
let body = &buf[(break_idx + 4)..]; async fn connect() -> Result<NamedPipeClient, std::io::Error> {
// apparently attempting to connect can fail if there's already a client connected
let creds_str = std::str::from_utf8(body) loop {
.map_err(|_| RequestError::MalformedHttpResponse)? match ClientOptions::new().open(r"\\.\pipe\creddy-requests") {
.to_string(); Ok(stream) => return Ok(stream),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
if creds_str == "Denied!" { Err(e) => return Err(e),
return Err(RequestError::Rejected); }
} tokio::time::sleep(Duration::from_millis(10)).await;
Ok(creds_str) }
}
#[cfg(unix)]
async fn connect() -> Result<UnixStream, std::io::Error> {
let path = Path::from("/tmp/creddy-requests");
std::fs::remove_file(path)?;
UnixStream::connect(path)
} }

View File

@ -1,76 +1,122 @@
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 std::os::windows::io::AsRawHandle;
use crate::{ #[cfg(windows)]
app::APP, use {
errors::*, tokio::net::windows::named_pipe::NamedPipeServer,
config::AppConfig, windows::Win32::{
state::AppState, Foundation::HANDLE,
System::Pipes::GetNamedPipeClientProcessId,
},
}; };
#[cfg(unix)]
use tokio::net::UnixStream;
use crate::errors::*;
#[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> { #[cfg(unix)]
let state = APP.get().unwrap().state::<AppState>(); pub fn get_client_parent(stream: &UnixStream) -> Result<Client, ClientInfoError> {
let AppConfig { let pid = stream.peer_cred()?;
listen_addr: app_listen_addr, get_process_parent_info(pid)?
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![])
} }
#[cfg(windows)]
pub fn get_client_parent(stream: &NamedPipeServer) -> Result<Client, ClientInfoError> {
let raw_handle = stream.as_raw_handle();
let mut pid = 0u32;
let handle = HANDLE(raw_handle as _);
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
get_process_parent_info(pid)
}
fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
let sys_pid = Pid::from_u32(pid);
let mut sys = System::new();
sys.refresh_process(sys_pid);
let proc = sys.process(sys_pid)
.ok_or(ClientInfoError::ProcessNotFound)?;
let parent_pid_sys = proc.parent()
.ok_or(ClientInfoError::ParentPidNotFound)?;
sys.refresh_process(parent_pid_sys);
let parent = sys.process(parent_pid_sys)
.ok_or(ClientInfoError::ParentProcessNotFound)?;
let exe = match parent.exe() {
p if p == Path::new("") => None,
p => Some(PathBuf::from(p)),
};
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
}
// 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)
} // }

View File

@ -1,4 +1,3 @@
use std::net::Ipv4Addr;
use std::path::PathBuf; use std::path::PathBuf;
use auto_launch::AutoLaunchBuilder; use auto_launch::AutoLaunchBuilder;
@ -42,10 +41,6 @@ pub struct HotkeysConfig {
#[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")]
@ -62,8 +57,6 @@ pub struct AppConfig {
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(),
@ -144,16 +137,6 @@ pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
} }
fn default_listen_port() -> u16 {
if cfg!(debug_assertions) {
12_345
}
else {
19_923
}
}
fn default_term_config() -> TermConfig { fn default_term_config() -> TermConfig {
#[cfg(windows)] #[cfg(windows)]
{ {
@ -238,7 +221,6 @@ pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
} }
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
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) }

View File

@ -162,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,
@ -198,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()
@ -206,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,
}; };
@ -230,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
{ {

View File

@ -2,6 +2,7 @@ use std::error::Error;
use std::convert::AsRef; use std::convert::AsRef;
use std::ffi::OsString; 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;
@ -17,7 +18,12 @@ 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 ErrorPopup {
@ -137,12 +143,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}")]
@ -151,6 +159,8 @@ 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,
} }
@ -207,26 +217,49 @@ 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,
#[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 {
fn from(s: ServerError) -> Self {
Self::Server(s)
}
} }
@ -298,13 +331,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()
} }
} }
@ -353,6 +379,8 @@ 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)?, _ => serialize_upstream_err(self, &mut map)?,
} }
map.end() map.end()

View File

@ -10,9 +10,9 @@ 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,
} }

View File

@ -16,12 +16,13 @@ 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),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Err(e) = res { if let Err(e) = res {
eprintln!("Error: {e}"); eprintln!("Error: {e}");
std::process::exit(1);
} }
} }

View File

@ -1,275 +1,184 @@
use core::time::Duration; use std::time::Duration;
use std::io;
use std::net::{ #[cfg(windows)]
Ipv4Addr, use tokio::net::windows::named_pipe::{
SocketAddr, NamedPipeServer,
SocketAddrV4, ServerOptions,
};
use tokio::net::{
TcpListener,
TcpStream,
}; };
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot::{self, Sender, Receiver}; use tokio::sync::oneshot;
use tokio::time::sleep;
use tauri::{AppHandle, Manager}; use serde::{Serialize, Deserialize};
use tauri::async_runtime as rt;
use tauri::async_runtime::JoinHandle; use tauri::{
AppHandle,
Manager,
async_runtime as rt,
};
use crate::{clientinfo, clientinfo::Client};
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Request, Approval}; use crate::clientinfo::{self, Client};
use crate::credentials::Credentials;
use crate::ipc::{Approval, AwsRequestNotification};
use crate::state::AppState; use crate::state::AppState;
#[derive(Debug)] #[derive(Serialize, Deserialize)]
pub struct RequestWaiter { pub enum Request {
pub rehide_after: bool, GetAwsCredentials{
pub sender: Option<Sender<Approval>>, base: bool,
} },
impl RequestWaiter {
pub fn notify(&mut self, approval: Approval) -> Result<(), SendResponseError> {
let chan = self.sender
.take()
.ok_or(SendResponseError::Fulfilled)?;
chan.send(approval)
.map_err(|_| SendResponseError::Abandoned)
}
} }
struct Handler { #[derive(Debug, Serialize, Deserialize)]
request_id: u64, pub enum Response {
stream: TcpStream, Aws(Credentials)
rehide_after: bool,
receiver: Option<Receiver<Approval>>,
app: AppHandle,
}
impl Handler {
async fn new(stream: TcpStream, app: AppHandle) -> Result<Self, HandlerError> {
let state = app.state::<AppState>();
// determine whether we should re-hide the window after handling this request
let is_currently_visible = app.get_window("main")
.ok_or(HandlerError::NoMainWindow)?
.is_visible()?;
let rehide_after = state.current_rehide_status()
.await
.unwrap_or(!is_currently_visible);
let (chan_send, chan_recv) = oneshot::channel();
let waiter = RequestWaiter {rehide_after, sender: Some(chan_send)};
let request_id = state.register_request(waiter).await;
let handler = Handler {
request_id,
stream,
rehide_after,
receiver: Some(chan_recv),
app
};
Ok(handler)
}
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)?;
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 self.rehide_after && state.req_count().await == 1 {
self.app
.get_window("main")
.ok_or(HandlerError::NoMainWindow)?
.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<(), HandlerError> {
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
if !window.is_visible()? {
window.unminimize()?;
window.show()?;
}
window.set_focus()?;
Ok(())
}
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 { pub struct Server {
addr: Ipv4Addr, listener: tokio::net::windows::named_pipe::NamedPipeServer,
port: u16,
app_handle: AppHandle, app_handle: AppHandle,
task: JoinHandle<()>,
} }
impl Server { impl Server {
pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<Server> { pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
let task = Self::start_server(addr, port, app_handle.app_handle()).await?; let listener = ServerOptions::new()
Ok(Server { addr, port, app_handle, task}) .first_pipe_instance(true)
} .create(r"\\.\pipe\creddy-requests")?;
pub async fn rebind(&mut self, addr: Ipv4Addr, port: u16) -> io::Result<()> { let srv = Server {listener, app_handle};
if addr == self.addr && port == self.port { rt::spawn(srv.serve());
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(()) Ok(())
} }
// construct the listener before spawning the task so that we can return early if it fails async fn serve(mut self) {
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 { loop {
match listener.accept().await { if let Err(e) = self.try_serve().await {
Ok((stream, _)) => { eprintln!("Error accepting connection: {e}");
match Handler::new(stream, app_handle.app_handle()).await {
Ok(handler) => { rt::spawn(handler.handle()); }
Err(e) => { eprintln!("Error handling request: {e}"); }
}
},
Err(e) => { eprintln!("Error accepting connection: {e}"); }
} }
} }
} }
async fn try_serve(&mut self) -> std::io::Result<()> {
// 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();
rt::spawn(async move {
let res = serde_json::to_string(
&handle(&mut stream, new_handle).await
).unwrap();
if let Err(e) = stream.write_all(res.as_bytes()).await {
eprintln!("Error responding to request: {e}");
}
});
Ok(())
}
}
async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result<Response, 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_client_parent(&stream)?;
let req: Request = serde_json::from_slice(&buf)?;
match req {
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
// etc
}
}
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
let state = app_handle.state::<AppState>();
let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?;
let is_currently_visible = main_window.is_visible()?;
let rehide_after = state.get_or_set_rehide(!is_currently_visible).await;
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", &notification)?;
if !main_window.is_visible()? {
main_window.unminimize()?;
main_window.show()?;
}
main_window.set_focus()?;
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)
}
};
rt::spawn(
handle_rehide(rehide_after, app_handle.app_handle())
);
result
}
async fn handle_rehide(rehide_after: bool, app_handle: AppHandle) {
let state = app_handle.state::<AppState>();
let delay = {
let config = state.config.read().await;
Duration::from_millis(config.rehide_ms)
};
tokio::time::sleep(delay).await;
// if there are no other pending requests, set rehide status back to None
if state.req_count().await == 0 {
state.clear_rehide().await;
// and hide the window if necessary
if rehide_after {
app_handle.get_window("main").map(|w| {
if let Err(e) = w.hide() {
eprintln!("{e}");
}
});
}
}
} }

View File

@ -0,0 +1,51 @@
use serde::{Serialize, Deserialize};
use tauri::{
AppHandle,
Manager,
};
use crate::app::APP;
use crate::config::HotkeysConfig;
use crate::terminal;
#[derive(Debug, Serialize, Deserialize)]
pub enum ShortcutAction {
ShowWindow,
LaunchTerminal,
}
pub fn exec_shortcut(action: ShortcutAction) {
match action {
ShowWindow => {
let app = APP.get().unwrap();
app.get_window("main").map(|w| w.show());
},
LaunchTerminal => terminal::launch(false),
}
}
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(())
}

View File

@ -1,15 +1,11 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use std::time::Duration;
use tokio::{ use tokio::{
sync::RwLock, sync::RwLock,
time::sleep, sync::oneshot::Sender,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tauri::async_runtime as runtime;
use tauri::Manager;
use crate::app::APP;
use crate::credentials::{ use crate::credentials::{
Session, Session,
BaseCredentials, BaseCredentials,
@ -17,9 +13,7 @@ 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, RequestWaiter};
#[derive(Debug)] #[derive(Debug)]
@ -27,12 +21,11 @@ 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 waiting_requests: RwLock<HashMap<u64, RequestWaiter>>, pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
pub current_rehide_status: RwLock<Option<bool>>,
pub pending_terminal_request: RwLock<bool>, pub pending_terminal_request: RwLock<bool>,
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
// setup_errors is never modified and so doesn't need to be wrapped in RwLock // setup_errors is never modified and so doesn't need to be wrapped in RwLock
pub setup_errors: Vec<String>, pub setup_errors: Vec<String>,
server: RwLock<Server>,
pool: sqlx::SqlitePool, pool: sqlx::SqlitePool,
} }
@ -40,7 +33,6 @@ impl AppState {
pub fn new( pub fn new(
config: AppConfig, config: AppConfig,
session: Session, session: Session,
server: Server,
pool: SqlitePool, pool: SqlitePool,
setup_errors: Vec<String>, setup_errors: Vec<String>,
) -> AppState { ) -> AppState {
@ -49,10 +41,9 @@ impl AppState {
session: RwLock::new(session), session: RwLock::new(session),
request_count: RwLock::new(0), request_count: RwLock::new(0),
waiting_requests: RwLock::new(HashMap::new()), waiting_requests: RwLock::new(HashMap::new()),
current_rehide_status: RwLock::new(None),
pending_terminal_request: RwLock::new(false), pending_terminal_request: RwLock::new(false),
bans: RwLock::new(HashSet::new()),
setup_errors, setup_errors,
server: RwLock::new(server),
pool, pool,
} }
} }
@ -73,13 +64,7 @@ impl AppState {
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)?;
} }
// rebind socket if necessary
if new_config.listen_addr != live_config.listen_addr
|| new_config.listen_port != live_config.listen_port
{
let mut sv = self.server.write().await;
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
}
// re-register hotkeys if necessary // re-register hotkeys if necessary
if new_config.hotkeys.show_window != live_config.hotkeys.show_window if new_config.hotkeys.show_window != live_config.hotkeys.show_window
|| new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal || new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal
@ -92,7 +77,7 @@ impl AppState {
Ok(()) Ok(())
} }
pub async fn register_request(&self, waiter: RequestWaiter) -> 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;
@ -100,7 +85,7 @@ impl AppState {
}; };
let mut waiting_requests = self.waiting_requests.write().await; let mut waiting_requests = self.waiting_requests.write().await;
waiting_requests.insert(*count, waiter); // `count` is the request id waiting_requests.insert(*count, sender); // `count` is the request id
*count *count
} }
@ -114,11 +99,20 @@ impl AppState {
waiting_requests.len() waiting_requests.len()
} }
pub async fn current_rehide_status(&self) -> Option<bool> { pub async fn get_or_set_rehide(&self, new_value: bool) -> bool {
// since all requests that are pending at a given time should have the same let mut rehide = self.current_rehide_status.write().await;
// value for rehide_after, it doesn't matter which one we use match *rehide {
let waiting_requests = self.waiting_requests.read().await; Some(original) => original,
waiting_requests.iter().next().map(|(_id, w)| w.rehide_after) None => {
*rehide = Some(new_value);
new_value
}
}
}
pub async fn clear_rehide(&self) {
let mut rehide = self.current_rehide_status.write().await;
*rehide = None;
} }
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
@ -129,26 +123,10 @@ impl AppState {
let mut waiting_requests = self.waiting_requests.write().await; let mut waiting_requests = self.waiting_requests.write().await;
waiting_requests waiting_requests
.get_mut(&response.id) .remove(&response.id)
.ok_or(SendResponseError::NotFound)? .ok_or(SendResponseError::NotFound)?
.notify(response.approval) .send(response.approval)
} .map_err(|_| 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> {
@ -168,16 +146,16 @@ impl AppState {
matches!(*session, Session::Unlocked{..}) matches!(*session, Session::Unlocked{..})
} }
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> { pub async fn base_creds_cloned(&self) -> Result<BaseCredentials, GetCredentialsError> {
let app_session = self.session.read().await; let app_session = self.session.read().await;
let (base, _session) = app_session.try_get()?; let (base, _session) = app_session.try_get()?;
Ok(serde_json::to_string(base).unwrap()) Ok(base.clone())
} }
pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> { pub async fn session_creds_cloned(&self) -> Result<SessionCredentials, GetCredentialsError> {
let app_session = self.session.read().await; let app_session = self.session.read().await;
let (_bsae, session) = app_session.try_get()?; let (_bsae, session) = app_session.try_get()?;
Ok(serde_json::to_string(session).unwrap()) Ok(session.clone())
} }
async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> { async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {

View File

@ -63,7 +63,7 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
else { else {
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); 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_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
cmd.env("AWS_SESSION_TOKEN", &session_creds.token); cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);
} }
} }

View File

@ -47,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
@ -97,12 +94,10 @@
<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>

View File

@ -58,18 +58,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>