Compare commits

..

2 Commits

12 changed files with 238 additions and 191 deletions

3
.gitignore vendored
View File

@ -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*

View File

@ -1 +0,0 @@
DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db

View File

@ -1,5 +1,6 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::process::Command as ChildCommand; use std::process::Command as ChildCommand;
#[cfg(windows)]
use std::time::Duration; use std::time::Duration;
use clap::{ use clap::{
@ -19,7 +20,6 @@ use crate::shortcuts::ShortcutAction;
#[cfg(unix)] #[cfg(unix)]
use { use {
std::os::unix::process::CommandExt, std::os::unix::process::CommandExt,
std::path::Path,
tokio::net::UnixStream, tokio::net::UnixStream,
}; };
@ -199,7 +199,5 @@ async fn connect() -> Result<NamedPipeClient, std::io::Error> {
#[cfg(unix)] #[cfg(unix)]
async fn connect() -> Result<UnixStream, std::io::Error> { async fn connect() -> Result<UnixStream, std::io::Error> {
let path = Path::from("/tmp/creddy-requests"); UnixStream::connect("/tmp/creddy.sock").await
std::fs::remove_file(path)?;
UnixStream::connect(path)
} }

View File

@ -2,19 +2,6 @@ use std::path::{Path, PathBuf};
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;
#[cfg(windows)]
use {
tokio::net::windows::named_pipe::NamedPipeServer,
windows::Win32::{
Foundation::HANDLE,
System::Pipes::GetNamedPipeClientProcessId,
},
};
#[cfg(unix)]
use tokio::net::UnixStream;
use crate::errors::*; use crate::errors::*;
@ -26,25 +13,8 @@ pub struct Client {
} }
#[cfg(unix)] pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
pub fn get_client_parent(stream: &UnixStream) -> Result<Client, ClientInfoError> { dbg!(pid);
let pid = stream.peer_cred()?;
get_process_parent_info(pid)?
}
#[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 sys_pid = Pid::from_u32(pid);
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_process(sys_pid); sys.refresh_process(sys_pid);

View File

@ -4,11 +4,6 @@ use auto_launch::AutoLaunchBuilder;
use is_terminal::IsTerminal; use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tauri::{
Manager,
GlobalShortcutManager,
async_runtime as rt,
};
use crate::errors::*; use crate::errors::*;
@ -183,43 +178,6 @@ fn default_hotkey_config() -> HotkeysConfig {
} }
} }
// note: will panic if called before APP is set
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
let app = crate::app::APP.get().unwrap();
let mut manager = app.global_shortcut_manager();
manager.unregister_all()?;
if hotkeys.show_window.enabled {
let handle = app.app_handle();
manager.register(
&hotkeys.show_window.keys,
move || {
handle.get_window("main")
.map(|w| w.show().error_popup("Failed to show"))
.ok_or(HandlerError::NoMainWindow)
.error_popup("No main window");
},
)?;
}
if hotkeys.launch_terminal.enabled {
// register() doesn't take an async fn, so we have to use spawn
manager.register(
&hotkeys.launch_terminal.keys,
|| {
rt::spawn(async {
crate::terminal::launch(false)
.await
.error_popup("Failed to launch");
});
}
)?;
}
Ok(())
}
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

View File

@ -244,6 +244,7 @@ pub enum ClientInfoError {
ParentPidNotFound, ParentPidNotFound,
#[error("Found PID for parent process of client, but no corresponding process")] #[error("Found PID for parent process of client, but no corresponding process")]
ParentProcessNotFound, ParentProcessNotFound,
#[cfg(windows)]
#[error("Could not determine PID of connected client")] #[error("Could not determine PID of connected client")]
WindowsError(#[from] windows::core::Error), WindowsError(#[from] windows::core::Error),
#[error(transparent)] #[error(transparent)]

View File

@ -18,6 +18,7 @@ fn main() {
}, },
Some(("get", m)) => cli::get(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!(),
}; };

View File

@ -1,18 +1,9 @@
#[cfg(windows)]
use tokio::net::windows::named_pipe::{
NamedPipeServer,
ServerOptions,
};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tauri::{ use tauri::{AppHandle, Manager};
AppHandle,
Manager,
async_runtime as rt,
};
use crate::errors::*; use crate::errors::*;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
@ -21,6 +12,20 @@ use crate::ipc::{Approval, AwsRequestNotification};
use crate::state::AppState; use crate::state::AppState;
use crate::shortcuts::{self, ShortcutAction}; 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)] #[derive(Serialize, Deserialize)]
pub enum Request { pub enum Request {
@ -38,53 +43,8 @@ pub enum Response {
} }
pub struct Server { async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
listener: tokio::net::windows::named_pipe::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) -> 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 // 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 buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
let mut n = 0; let mut n = 0;
@ -98,13 +58,17 @@ async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result<R
} }
} }
let client = clientinfo::get_client_parent(&stream)?; let client = clientinfo::get_process_parent_info(client_pid)?;
let req: Request = serde_json::from_slice(&buf)?; let req: Request = serde_json::from_slice(&buf)?;
match req { let res = match req {
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await, Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
Request::InvokeShortcut(action) => invoke_shortcut(action).await, Request::InvokeShortcut(action) => invoke_shortcut(action).await,
} };
let res = serde_json::to_vec(&res).unwrap();
stream.write_all(&res).await?;
Ok(())
} }

View 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)
}

View 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
}

View File

@ -10,15 +10,21 @@
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(
@ -28,7 +34,7 @@
updateValue(pendingValue); updateValue(pendingValue);
} }
}, },
500 debounceInterval,
) )
} }

View File

@ -14,15 +14,19 @@
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() {
console.log('updating config'); console.log('updating config');
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');
} }
} }
@ -35,62 +39,60 @@
<h1 slot="title" class="text-2xl font-bold">Settings</h1> <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-lg mx-auto mt-1.5 p-4 space-y-16"> <SettingsGroup name="General">
<SettingsGroup name="General"> <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
to tray before the request was received. to tray before the request was received.
</svelte:fragment> </svelte:fragment>
</NumericSetting> </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>
</Link> </Link>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Update or re-enter your encrypted credentials. Update or re-enter your encrypted credentials.
</svelte:fragment> </svelte:fragment>
</Setting> </Setting>
<FileSetting <FileSetting
title="Terminal emulator" title="Terminal emulator"
bind:value={$appState.config.terminal.exec} bind:value={config.terminal.exec}
on:update={save}
> >
<svelte:fragment slot="description"> <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>. 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> </svelte:fragment>
</FileSetting> </FileSetting>
</SettingsGroup> </SettingsGroup>
<SettingsGroup name="Hotkeys"> <SettingsGroup name="Hotkeys">
<div class="space-y-4"> <div class="space-y-4">
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p> <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"> <div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
<Keybind description="Show Creddy" value={$appState.config.hotkeys.show_window} on:update={save} /> <Keybind description="Show Creddy" bind:value={config.hotkeys.show_window} />
<Keybind description="Launch terminal" value={$appState.config.hotkeys.launch_terminal} on:update={save} /> <Keybind description="Launch terminal" bind:value={config.hotkeys.launch_terminal} />
</div>
</div> </div>
</SettingsGroup> </div>
</SettingsGroup>
</div> </div>
{/await}
{#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">
@ -104,4 +106,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}