32 Commits

Author SHA1 Message Date
33a5600a30 prevent NumericSetting from accepting non-numeric inputs 2023-04-27 16:15:19 -07:00
741169d807 start on login 2023-04-27 14:24:08 -07:00
ebc00a5df6 confirm passphrase 2023-04-26 17:13:58 -07:00
c2cc007a81 display tweaks and approval page timing 2023-04-26 17:06:37 -07:00
4aab08e6f0 save settings to db 2023-04-26 15:49:08 -07:00
12d9d733a5 fix circular imports from routing 2023-04-26 13:05:51 -07:00
35271049dd settings page 2023-04-25 22:10:28 -07:00
6f9cd6b471 move app state to store 2023-04-25 08:49:00 -07:00
865b7fd5c4 add settings page 2023-04-24 22:18:55 -07:00
f35352eedd links, navs, and more 2023-04-24 22:16:25 -07:00
53580d7919 rework routing 2023-04-24 12:05:11 -07:00
049b81610d rewrite frontend with DaisyUI 2023-04-23 22:29:12 -07:00
fd60899f16 don't re-hide when a request comes in while showing approval screen 2023-04-21 11:18:20 -07:00
e0c4c849dc serializable structured errors 2022-12-29 16:40:48 -08:00
cb26201506 unproductive flailing 2022-12-28 15:48:25 -08:00
992e3c8db2 build improvements 2022-12-28 10:16:06 -08:00
4956b64371 only print incoming requests in debug mode 2022-12-28 08:54:08 -08:00
df6b362a31 return structured errors from commands (wip) 2022-12-23 11:34:17 -08:00
2943634248 button component 2022-12-22 21:50:09 -08:00
06f5a1af42 icon picker component 2022-12-22 19:53:14 -08:00
61d674199f store config in database, macro for state access 2022-12-22 16:36:32 -08:00
398916fe10 use different ports for dev and live modes 2022-12-21 16:22:24 -08:00
bf4c46238e move re-hide to main request handler 2022-12-21 16:04:12 -08:00
5ffa55c03c basic system tray functionality 2022-12-21 14:49:01 -08:00
50f0985f4f display client info and unlock errors to user 2022-12-21 13:43:37 -08:00
69475604c0 update npm deps 2022-12-21 11:02:19 -08:00
856b6f1e1b use thiserror for errors 2022-12-21 11:01:34 -08:00
414379b74e completely reorganize http server 2022-12-20 16:11:49 -08:00
80b92ebe69 generalize pid conversion 2022-12-20 13:01:44 -08:00
983d0e8639 autofocus passphrase field in unlock view 2022-12-19 20:45:26 -08:00
d77437cda8 ban list 2022-12-19 16:20:46 -08:00
3d5cbedae1 working basic flow 2022-12-19 15:26:44 -08:00
45 changed files with 4070 additions and 1147 deletions

View File

@ -1,25 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte</title> <title>Vite + Svelte</title>
<style>
body {
margin: 0;
display: grid;
align-items: center;
justify-items: center;
min-width: 100vw;
min-height: 100vh;
}
</style>
</head> </head>
<body class="bg-zinc-800"> <body id="app" class="m-0">
<div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

1727
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"vite": "^3.0.7" "vite": "^3.0.7"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.0.2" "@tauri-apps/api": "^1.0.2",
"daisyui": "^2.51.5"
} }
} }

View File

@ -0,0 +1,7 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

1
src-tauri/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=sqlite://creddy.db?mode=rwc

673
src-tauri/Cargo.lock generated
View File

@ -68,14 +68,23 @@ checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
name = "app" name = "app"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"auto-launch",
"aws-config",
"aws-sdk-sts",
"aws-smithy-types",
"aws-types",
"netstat2", "netstat2",
"once_cell",
"serde", "serde",
"serde_json", "serde_json",
"sodiumoxide", "sodiumoxide",
"sqlx", "sqlx",
"strum 0.24.1",
"strum_macros 0.24.3",
"sysinfo", "sysinfo",
"tauri", "tauri",
"tauri-build", "tauri-build",
"thiserror",
"tokio", "tokio",
] ]
@ -130,18 +139,305 @@ dependencies = [
"wildmatch", "wildmatch",
] ]
[[package]]
name = "auto-launch"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5904a4d734f0235edf29aab320a14899f3e090446e594ff96508a6215f76f89c"
dependencies = [
"dirs",
"thiserror",
"winreg",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7688e1dfbb9f7804fab0a830820d7e827b8d973906763cf1a855ce4719292f5"
dependencies = [
"aws-http",
"aws-sdk-sso",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"hex",
"http",
"hyper",
"ring",
"time",
"tokio",
"tower",
"tracing",
"zeroize",
]
[[package]]
name = "aws-endpoint"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "253d7cd480bfa59a5323390e9e91885a8f06a275e0517d81eeb1070b6aa7d271"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-http"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd1b83859383e46ea8fda633378f9f3f02e6e3a446fd89f0240b5c3662716c9"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http-body",
"lazy_static",
"percent-encoding",
"pin-project-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf03342c2b3f52b180f484e60586500765474f2bfc7dcd4ffe893a7a1929db1d"
dependencies = [
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-json",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"tokio-stream",
"tower",
]
[[package]]
name = "aws-sdk-sts"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa1de4e07ea87a30a317c7b563b3a40fd18a843ad794216dda81672b6e174bce"
dependencies = [
"aws-endpoint",
"aws-http",
"aws-sig-auth",
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-query",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"http",
"tower",
"tracing",
]
[[package]]
name = "aws-sig-auth"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6126c4ff918e35fb9ae1bf2de71157fad36f0cc6a2b1d0f7197ee711713700fc"
dependencies = [
"aws-sigv4",
"aws-smithy-http",
"aws-types",
"http",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c7f88d7395f5411c6eef5889b6cd577ce6b677af461356cbfc20176c26c160"
dependencies = [
"aws-smithy-http",
"form_urlencoded",
"hex",
"hmac",
"http",
"once_cell",
"percent-encoding",
"regex",
"sha2",
"time",
"tracing",
]
[[package]]
name = "aws-smithy-async"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e6a895d68852dd1564328e63ef1583e5eb307dd2a5ebf35d862a5c402957d5e"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
"tokio-stream",
]
[[package]]
name = "aws-smithy-client"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f505bf793eb3e6d7c166ef1275c27b4b2cd5361173fe950ac8e2cfc08c29a7ef"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-http-tower",
"aws-smithy-types",
"bytes",
"fastrand",
"http",
"http-body",
"hyper",
"hyper-rustls",
"lazy_static",
"pin-project-lite",
"tokio",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-http"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e4b4304b7ea4af1af3e08535100eb7b6459d5a6264b92078bf85176d04ab85"
dependencies = [
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"hyper",
"once_cell",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "aws-smithy-http-tower"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86072ecc4dc4faf3e2071144285cfd539263fe7102b701d54fb991eafb04af8"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower",
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e3ddd9275b167bc59e9446469eca56177ec0b51225632f90aaa2cd5f41c940e"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b19d2e0b3ce20e460bad0d0d974238673100edebba6978c2c1aadd925602f7"
dependencies = [
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-types"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "987b1e37febb9bd409ca0846e82d35299e572ad8279bc404778caeb5fc05ad56"
dependencies = [
"base64-simd",
"itoa 1.0.2",
"num-integer",
"ryu",
"time",
]
[[package]]
name = "aws-smithy-xml"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ce3791e14eec75ffac851a5a559f1ce6b31843297f42cc8bfba82714a6a5d8"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c05adca3e2bcf686dd2c47836f216ab52ed7845c177d180c84b08522c1166a3"
dependencies = [
"aws-smithy-async",
"aws-smithy-client",
"aws-smithy-http",
"aws-smithy-types",
"http",
"rustc_version 0.4.0",
"tracing",
"zeroize",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64-simd"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
dependencies = [
"simd-abstraction",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -217,6 +513,16 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "bytes-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
dependencies = [
"bytes",
"either",
]
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.15.12" version = "0.15.12"
@ -618,6 +924,16 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
] ]
[[package]] [[package]]
@ -630,6 +946,17 @@ dependencies = [
"dirs-sys-next", "dirs-sys-next",
] ]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "dirs-sys-next" name = "dirs-sys-next"
version = "0.1.2" version = "0.1.2"
@ -736,7 +1063,7 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall",
"windows-sys", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -1203,6 +1530,25 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "h2"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -1254,6 +1600,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.25.2" version = "0.25.2"
@ -1279,12 +1634,75 @@ dependencies = [
"itoa 1.0.2", "itoa 1.0.2",
] ]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]] [[package]]
name = "http-range" name = "http-range"
version = "0.1.5" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa 1.0.2",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
dependencies = [
"http",
"hyper",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
"webpki-roots",
]
[[package]] [[package]]
name = "ico" name = "ico"
version = "0.1.0" version = "0.1.0"
@ -1496,6 +1914,30 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libappindicator"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
dependencies = [
"glib",
"gtk",
"gtk-sys",
"libappindicator-sys",
"log",
]
[[package]]
name = "libappindicator-sys"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
dependencies = [
"gtk-sys",
"libloading",
"once_cell",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.126" version = "0.2.126"
@ -1511,6 +1953,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]] [[package]]
name = "libsodium-sys" name = "libsodium-sys"
version = "0.2.7" version = "0.2.7"
@ -1682,7 +2134,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -1920,9 +2372,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]] [[package]]
name = "open" name = "open"
@ -1931,7 +2383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045" checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045"
dependencies = [ dependencies = [
"pathdiff", "pathdiff",
"windows-sys", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -2000,6 +2452,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "outref"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.15.10" version = "0.15.10"
@ -2076,7 +2534,7 @@ dependencies = [
"libc", "libc",
"redox_syscall", "redox_syscall",
"smallvec", "smallvec",
"windows-sys", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -2589,6 +3047,18 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.1" version = "1.0.1"
@ -2632,7 +3102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -2880,6 +3350,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4"
[[package]]
name = "simd-abstraction"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
dependencies = [
"outref",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.10" version = "0.3.10"
@ -3126,9 +3605,15 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.22.0",
] ]
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.22.0" version = "0.22.0"
@ -3141,6 +3626,25 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.0",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.98" version = "1.0.98"
@ -3206,6 +3710,7 @@ dependencies = [
"core-foundation", "core-foundation",
"core-graphics", "core-graphics",
"crossbeam-channel", "crossbeam-channel",
"dirs-next",
"dispatch", "dispatch",
"gdk", "gdk",
"gdk-pixbuf", "gdk-pixbuf",
@ -3219,6 +3724,7 @@ dependencies = [
"instant", "instant",
"jni 0.19.0", "jni 0.19.0",
"lazy_static", "lazy_static",
"libappindicator",
"libc", "libc",
"log", "log",
"ndk", "ndk",
@ -3459,18 +3965,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.31" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.31" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3514,9 +4020,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.20.1" version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -3524,13 +4030,12 @@ dependencies = [
"memchr", "memchr",
"mio", "mio",
"num_cpus", "num_cpus",
"once_cell",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"winapi", "windows-sys 0.42.0",
] ]
[[package]] [[package]]
@ -3566,6 +4071,20 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.9" version = "0.5.9"
@ -3575,6 +4094,34 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.36" version = "0.1.36"
@ -3582,6 +4129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@ -3646,6 +4194,12 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.15.0" version = "1.15.0"
@ -3710,6 +4264,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -3798,6 +4358,16 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"
@ -4104,12 +4674,33 @@ dependencies = [
"windows_x86_64_msvc 0.36.1", "windows_x86_64_msvc 0.36.1",
] ]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]] [[package]]
name = "windows-tokens" name = "windows-tokens"
version = "0.37.0" version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.32.0" version = "0.32.0"
@ -4128,6 +4719,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.24.0" version = "0.24.0"
@ -4152,6 +4749,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.24.0" version = "0.24.0"
@ -4176,6 +4779,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.24.0" version = "0.24.0"
@ -4200,6 +4809,18 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.24.0" version = "0.24.0"
@ -4224,6 +4845,12 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@ -4248,7 +4875,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007a0353840b23e0c6dc73e5b962ff58ed7f6bc9ceff3ce7fe6fbad8d496edf4" checksum = "007a0353840b23e0c6dc73e5b962ff58ed7f6bc9ceff3ce7fe6fbad8d496edf4"
dependencies = [ dependencies = [
"strum", "strum 0.22.0",
"windows 0.24.0", "windows 0.24.0",
"xml-rs", "xml-rs",
] ]
@ -4320,3 +4947,15 @@ name = "xml-rs"
version = "0.8.4" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmlparser"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"

View File

@ -17,12 +17,21 @@ 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.0.5", features = ["api-all"] } tauri = { version = "1.0.5", features = ["api-all", "system-tray"] }
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" netstat2 = "0.9.1"
sysinfo = "0.26.8" sysinfo = "0.26.8"
aws-types = "0.52.0"
aws-sdk-sts = "0.22.0"
aws-smithy-types = "0.52.0"
aws-config = "0.52.0"
thiserror = "1.0.38"
once_cell = "1.16.0"
strum = "0.24"
strum_macros = "0.24"
auto-launch = "0.4.0"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
@ -31,3 +40,6 @@ default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem # this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this # DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ] custom-protocol = [ "tauri/custom-protocol" ]
# [profile.dev.build-override]
# opt-level = 3

View File

@ -3,12 +3,13 @@ CREATE TABLE credentials (
access_key_id TEXT NOT NULL, access_key_id TEXT NOT NULL,
secret_key_enc BLOB NOT NULL, secret_key_enc BLOB NOT NULL,
salt BLOB NOT NULL, salt BLOB NOT NULL,
nonce BLOB NOT NULL nonce BLOB NOT NULL,
created_at INTEGER NOT NULL
); );
CREATE TABLE config ( CREATE TABLE config (
name TEXT, name TEXT UNIQUE NOT NULL,
data TEXT data TEXT NOT NULL
); );
CREATE TABLE clients ( CREATE TABLE clients (

View File

@ -1,17 +1,26 @@
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo}; use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use sysinfo::{System, SystemExt, Pid, ProcessExt}; use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
use serde::{Serialize, Deserialize};
use crate::errors::*; use crate::errors::*;
use crate::ipc::Client; use crate::get_state;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct Client {
pub pid: u32,
pub exe: String,
}
fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> { fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
let mut it = netstat2::iterate_sockets_info( let sockets_iter = netstat2::iterate_sockets_info(
AddressFamilyFlags::IPV4, AddressFamilyFlags::IPV4,
ProtocolFlags::TCP ProtocolFlags::TCP
)?; )?;
for (i, item) in it.enumerate() { get_state!(config as app_config);
for item in sockets_iter {
let sock_info = item?; let sock_info = item?;
let proto_info = match sock_info.protocol_socket_info { let proto_info = match sock_info.protocol_socket_info {
ProtocolSocketInfo::Tcp(tcp_info) => tcp_info, ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
@ -19,9 +28,9 @@ fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Err
}; };
if proto_info.local_port == local_port if proto_info.local_port == local_port
&& proto_info.remote_port == 12345 && proto_info.remote_port == app_config.listen_port
&& proto_info.local_addr == std::net::Ipv4Addr::LOCALHOST && proto_info.local_addr == app_config.listen_addr
&& proto_info.remote_addr == std::net::Ipv4Addr::LOCALHOST && proto_info.remote_addr == app_config.listen_addr
{ {
return Ok(sock_info.associated_pids) return Ok(sock_info.associated_pids)
} }
@ -30,22 +39,26 @@ fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Err
} }
// Theoretically, on some systems, multiple processes can share a socket. We have to // Theoretically, on some systems, multiple processes can share a socket
// account for this even though 99% of the time there will be only one. pub fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
pub fn get_clients(local_port: u16) -> Result<Vec<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)? { for p in get_associated_pids(local_port)? {
let pid = Pid::from(p as usize); 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::PidNotFound)?; .ok_or(ClientInfoError::ProcessNotFound)?;
let client = Client { let client = Client {
pid: p, pid: p,
exe: proc.exe().to_string_lossy().into_owned(), exe: proc.exe().to_string_lossy().into_owned(),
}; };
clients.push(client); clients.push(Some(client));
} }
if clients.is_empty() {
clients.push(None);
}
Ok(clients) Ok(clients)
} }

122
src-tauri/src/config.rs Normal file
View File

@ -0,0 +1,122 @@
use std::net::Ipv4Addr;
use std::path::PathBuf;
use auto_launch::AutoLaunchBuilder;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;
use crate::errors::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
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")]
pub rehide_ms: u64,
#[serde(default = "default_start_minimized")]
pub start_minimized: bool,
#[serde(default = "default_start_on_login")]
pub start_on_login: bool,
}
impl Default for AppConfig {
fn default() -> Self {
AppConfig {
listen_addr: default_listen_addr(),
listen_port: default_listen_port(),
rehide_ms: default_rehide_ms(),
start_minimized: default_start_minimized(),
start_on_login: default_start_on_login(),
}
}
}
impl AppConfig {
pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
let res = sqlx::query!("SELECT * from config where name = 'main'")
.fetch_optional(pool)
.await?;
let row = match res {
Some(row) => row,
None => return Ok(AppConfig::default()),
};
Ok(serde_json::from_str(&row.data)?)
}
pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
let data = serde_json::to_string(self).unwrap();
sqlx::query(
"INSERT INTO config (name, data) VALUES ('main', ?)
ON CONFLICT (name) DO UPDATE SET data = ?"
)
.bind(&data)
.bind(&data)
.execute(pool)
.await?;
Ok(())
}
}
pub fn set_auto_launch(enable: bool) -> Result<(), SetupError> {
let path_buf = std::env::current_exe()
.map_err(|e| auto_launch::Error::Io(e))?;
let path = path_buf
.to_string_lossy();
let auto = AutoLaunchBuilder::new()
.set_app_name("Creddy")
.set_app_path(&path)
.build()?;
if enable {
auto.enable()?;
}
else {
auto.disable()?;
}
Ok(())
}
pub fn get_or_create_db_path() -> PathBuf {
if cfg!(debug_assertions) {
return PathBuf::from("./creddy.db");
}
let mut parent = std::env::var("HOME")
.map(|h| {
let mut p = PathBuf::from(h);
p.push(".config");
p
})
.unwrap_or(PathBuf::from("."));
parent.push("creddy.db");
parent
}
fn default_listen_port() -> u16 {
if cfg!(debug_assertions) {
12_345
}
else {
19_923
}
}
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
fn default_rehide_ms() -> u64 { 1000 }
// start minimized and on login only in production mode
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
fn default_start_on_login() -> bool { !cfg!(debug_assertions) }

View File

@ -1,144 +1,245 @@
use std::fmt::{Display, Formatter}; use std::error::Error;
use std::convert::From; use std::convert::AsRef;
use std::str::Utf8Error; use strum_macros::AsRefStr;
use thiserror::Error as ThisError;
use aws_sdk_sts::{
types::SdkError as AwsSdkError,
error::GetSessionTokenError,
};
use sqlx::{ use sqlx::{
error::Error as SqlxError, error::Error as SqlxError,
migrate::MigrateError, migrate::MigrateError,
}; };
use serde::{Serialize, Serializer, ser::SerializeMap};
// pub struct SerializeError<E> {
// pub err: E,
// }
// impl<E: std::error::Error> Serialize for SerializeError<E>
// {
// fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// let mut map = serializer.serialize_map(None)?;
// map.serialize_entry("msg", &format!("{}", self.err))?;
// if let Some(src) = self.err.source() {
// let ser_src = SerializeError { err: src };
// map.serialize_entry("source", &ser_src)?;
// }
// map.end()
// }
// }
// impl<E: std::error::Error> From<E> for SerializeError<E> {
// fn from(err: E) -> Self {
// SerializeError { err }
// }
// }
fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
where
E: std::error::Error + AsRef<str>,
S: Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", err.as_ref())?;
map.serialize_entry("msg", &format!("{err}"))?;
if let Some(src) = err.source() {
map.serialize_entry("source", &format!("{src}"))?;
}
map.end()
}
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
where
E: Error,
M: serde::ser::SerializeMap,
{
let src = err.source().map(|s| format!("{s}"));
map.serialize_entry("source", &src)
}
macro_rules! impl_serialize_basic {
($err_type:ident) => {
impl Serialize for $err_type {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serialize_basic_err(self, serializer)
}
}
}
}
// error during initial setup (primarily loading state from db) // error during initial setup (primarily loading state from db)
#[derive(Debug, ThisError, AsRefStr)]
pub enum SetupError { pub enum SetupError {
#[error("Invalid database record")]
InvalidRecord, // e.g. wrong size blob for nonce or salt InvalidRecord, // e.g. wrong size blob for nonce or salt
DbError(SqlxError), #[error("Error from database: {0}")]
} DbError(#[from] SqlxError),
impl From<SqlxError> for SetupError { #[error("Error running migrations: {0}")]
fn from(e: SqlxError) -> SetupError { MigrationError(#[from] MigrateError),
SetupError::DbError(e) #[error("Error parsing configuration from database")]
} ConfigParseError(#[from] serde_json::Error),
} #[error("Failed to set up start-on-login: {0}")]
impl From<MigrateError> for SetupError { AutoLaunchError(#[from] auto_launch::Error),
fn from (e: MigrateError) -> SetupError {
SetupError::DbError(SqlxError::from(e))
}
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
match self {
SetupError::InvalidRecord => write!(f, "Malformed database record"),
SetupError::DbError(e) => write!(f, "Error from database: {e}"),
}
}
} }
// error when attempting to tell a request handler whether to release or deny crednetials // error when attempting to tell a request handler whether to release or deny credentials
#[derive(Debug, ThisError, AsRefStr)]
pub enum SendResponseError { pub enum SendResponseError {
#[error("The specified credentials request was not found")]
NotFound, // no request with the given id NotFound, // no request with the given id
#[error("The specified request was already closed by the client")]
Abandoned, // request has already been closed by client Abandoned, // request has already been closed by client
} }
impl Display for SendResponseError {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
use SendResponseError::*;
match self {
NotFound => write!(f, "The specified command was not found."),
Abandoned => write!(f, "The specified request was closed by the client."),
}
}
}
// errors encountered while handling an HTTP request // errors encountered while handling an HTTP request
#[derive(Debug, ThisError, AsRefStr)]
pub enum RequestError { pub enum RequestError {
StreamIOError(std::io::Error), #[error("Error writing to stream: {0}")]
InvalidUtf8, StreamIOError(#[from] std::io::Error),
MalformedHttpRequest, // #[error("Received invalid UTF-8 in request")]
// InvalidUtf8,
// MalformedHttpRequest,
#[error("HTTP request too large")]
RequestTooLarge, RequestTooLarge,
NoCredentials(GetCredentialsError), #[error("Error accessing credentials: {0}")]
ClientInfo(ClientInfoError), NoCredentials(#[from] GetCredentialsError),
} #[error("Error getting client details: {0}")]
impl From<tokio::io::Error> for RequestError { ClientInfo(#[from] ClientInfoError),
fn from(e: std::io::Error) -> RequestError { #[error("Error from Tauri: {0}")]
RequestError::StreamIOError(e) Tauri(#[from] tauri::Error),
} #[error("No main application window found")]
} NoMainWindow,
impl From<Utf8Error> for RequestError {
fn from(_e: Utf8Error) -> RequestError {
RequestError::InvalidUtf8
}
}
impl From<GetCredentialsError> for RequestError {
fn from (e: GetCredentialsError) -> RequestError {
RequestError::NoCredentials(e)
}
}
impl From<ClientInfoError> for RequestError {
fn from(e: ClientInfoError) -> RequestError {
RequestError::ClientInfo(e)
}
}
impl Display for RequestError {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
use RequestError::*;
match self {
StreamIOError(e) => write!(f, "Stream IO error: {e}"),
InvalidUtf8 => write!(f, "Could not decode UTF-8 from bytestream"),
MalformedHttpRequest => write!(f, "Maformed HTTP request"),
RequestTooLarge => write!(f, "HTTP request too large"),
NoCredentials(GetCredentialsError::Locked) => write!(f, "Recieved go-ahead but app is locked"),
NoCredentials(GetCredentialsError::Empty) => write!(f, "Received go-ahead but no credentials are known"),
ClientInfo(ClientInfoError::PidNotFound) => write!(f, "Could not resolve PID of client process."),
ClientInfo(ClientInfoError::NetstatError(e)) => write!(f, "Error getting client socket details: {e}"),
}
}
} }
#[derive(Debug, ThisError, AsRefStr)]
pub enum GetCredentialsError { pub enum GetCredentialsError {
#[error("Credentials are currently locked")]
Locked, Locked,
#[error("No credentials are known")]
Empty, Empty,
} }
#[derive(Debug, ThisError, AsRefStr)]
pub enum GetSessionError {
#[error("Request completed successfully but no credentials were returned")]
NoCredentials, // SDK returned successfully but credentials are None
#[error("Error response from AWS SDK: {0}")]
SdkError(#[from] AwsSdkError<GetSessionTokenError>),
}
#[derive(Debug, ThisError, AsRefStr)]
pub enum UnlockError { pub enum UnlockError {
#[error("App is not locked")]
NotLocked, NotLocked,
#[error("No saved credentials were found")]
NoCredentials, NoCredentials,
#[error("Invalid passphrase")]
BadPassphrase, BadPassphrase,
#[error("Data was found to be corrupt after decryption")]
InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
DbError(SqlxError), #[error("Database error: {0}")]
} DbError(#[from] SqlxError),
impl From<SqlxError> for UnlockError { #[error("Failed to create AWS session: {0}")]
fn from (e: SqlxError) -> UnlockError { GetSession(#[from] GetSessionError),
match e {
SqlxError::RowNotFound => UnlockError::NoCredentials,
_ => UnlockError::DbError(e),
}
}
}
impl Display for UnlockError {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
use UnlockError::*;
match self {
NotLocked => write!(f, "App is not locked"),
NoCredentials => write!(f, "No saved credentials were found"),
BadPassphrase => write!(f, "Invalid passphrase"),
InvalidUtf8 => write!(f, "Decrypted data was corrupted"),
DbError(e) => write!(f, "Database error: {e}"),
}
}
} }
// Errors encountered while trying to figure out who's on the other end of a request // Errors encountered while trying to figure out who's on the other end of a request
#[derive(Debug, ThisError, AsRefStr)]
pub enum ClientInfoError { pub enum ClientInfoError {
PidNotFound, #[error("Found PID for client socket, but no corresponding process")]
NetstatError(netstat2::error::Error), ProcessNotFound,
#[error("Couldn't get client socket details: {0}")]
NetstatError(#[from] netstat2::error::Error),
} }
impl From<netstat2::error::Error> for ClientInfoError {
fn from(e: netstat2::error::Error) -> ClientInfoError {
ClientInfoError::NetstatError(e) // =========================
// Serialize implementations
// =========================
struct SerializeWrapper<E>(pub E);
impl Serialize for SerializeWrapper<&GetSessionTokenError> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let err = self.0;
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", &err.code())?;
map.serialize_entry("msg", &err.message())?;
map.serialize_entry("source", &None::<&str>)?;
map.end()
}
}
impl_serialize_basic!(SetupError);
impl_serialize_basic!(SendResponseError);
impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError);
impl Serialize for RequestError {
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 {
RequestError::NoCredentials(src) => map.serialize_entry("source", &src)?,
RequestError::ClientInfo(src) => map.serialize_entry("source", &src)?,
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
}
}
impl Serialize for GetSessionError {
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 {
GetSessionError::SdkError(AwsSdkError::ServiceError(se_wrapper)) => {
let err = se_wrapper.err();
map.serialize_entry("source", &SerializeWrapper(err))?
}
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
}
}
impl Serialize for UnlockError {
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 {
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
} }
} }

View File

@ -1,31 +1,27 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tauri::State; use tauri::State;
use crate::errors::*;
use crate::config::AppConfig;
use crate::clientinfo::Client;
use crate::state::{AppState, Session, Credentials}; use crate::state::{AppState, Session, Credentials};
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Client {
pub pid: u32,
pub exe: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Request { pub struct Request {
pub id: u64, pub id: u64,
pub clients: Vec<Client>, pub clients: Vec<Option<Client>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RequestResponse { pub struct RequestResponse {
pub id: u64, pub id: u64,
pub approval: Approval, pub approval: Approval,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Approval { pub enum Approval {
Approved, Approved,
Denied, Denied,
@ -40,10 +36,8 @@ pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Res
#[tauri::command] #[tauri::command]
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), String> { pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
app_state.decrypt(&passphrase) app_state.decrypt(&passphrase).await
.await
.map_err(|e| e.to_string())
} }
@ -63,8 +57,21 @@ pub async fn save_credentials(
credentials: Credentials, credentials: Credentials,
passphrase: String, passphrase: String,
app_state: State<'_, AppState> app_state: State<'_, AppState>
) -> Result<(), String> { ) -> Result<(), UnlockError> {
app_state.save_creds(credentials, &passphrase) app_state.save_creds(credentials, &passphrase).await
.await }
.map_err(|e| e.to_string())
#[tauri::command]
pub fn get_config(app_state: State<'_, AppState>) -> AppConfig {
let config = app_state.config.read().unwrap();
config.clone()
}
#[tauri::command]
pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> Result<(), String> {
app_state.update_config(config)
.await
.map_err(|e| format!("Error saving config to database: {e}"))
} }

View File

@ -3,35 +3,101 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use std::str::FromStr; use tauri::{AppHandle, Manager, async_runtime as rt};
use once_cell::sync::OnceCell;
mod config;
mod errors; mod errors;
mod clientinfo; mod clientinfo;
mod ipc; mod ipc;
mod state; mod state;
mod server; mod server;
mod storage; mod tray;
use crate::errors::*;
use state::AppState;
pub static APP: OnceCell<AppHandle> = OnceCell::new();
fn main() { fn main() {
let initial_state = match state::AppState::new() { let initial_state = match rt::block_on(AppState::load()) {
Ok(state) => state, Ok(state) => state,
Err(e) => {eprintln!("{}", e); return;} Err(e) => {eprintln!("{}", e); return;}
}; };
tauri::Builder::default() tauri::Builder::default()
.manage(initial_state) .manage(initial_state)
.system_tray(tray::create())
.on_system_tray_event(tray::handle_event)
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
ipc::unlock, ipc::unlock,
ipc::respond, ipc::respond,
ipc::get_session_status, ipc::get_session_status,
ipc::save_credentials, ipc::save_credentials,
ipc::get_config,
ipc::save_config,
]) ])
.setup(|app| { .setup(|app| {
let addr = std::net::SocketAddrV4::from_str("127.0.0.1:12345").unwrap(); APP.set(app.handle()).unwrap();
let state = app.state::<AppState>();
let config = state.config.read().unwrap();
config::set_auto_launch(config.start_on_login)?;
let addr = std::net::SocketAddrV4::new(config.listen_addr, config.listen_port);
tauri::async_runtime::spawn(server::serve(addr, app.handle())); tauri::async_runtime::spawn(server::serve(addr, app.handle()));
if !config.start_minimized {
app.get_window("main")
.ok_or(RequestError::NoMainWindow)?
.show()?;
}
Ok(()) Ok(())
}) })
.run(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application")
.run(|app, run_event| match run_event {
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
let _ = app.get_window(&label).map(|w| w.hide());
api.prevent_close();
}
_ => ()
}
_ => ()
})
} }
macro_rules! get_state {
($prop:ident as $name:ident) => {
use tauri::Manager;
let app = crate::APP.get().unwrap(); // as long as the app is running, this is fine
let state = app.state::<crate::state::AppState>();
let $name = state.$prop.read().unwrap(); // only panics if another thread has already panicked
};
(config.$prop:ident as $name:ident) => {
use tauri::Manager;
let app = crate::APP.get().unwrap();
let state = app.state::<crate::state::AppState>();
let config = state.config.read().unwrap();
let $name = config.$prop;
};
(mut $prop:ident as $name:ident) => {
use tauri::Manager;
let app = crate::APP.get().unwrap();
let state = app.state::<crate::state::AppState>();
let $name = state.$prop.write().unwrap();
};
(mut config.$prop:ident as $name:ident) => {
use tauri::Manager;
let app = crate::APP.get().unwrap();
let state = app.state::<crate::state::AppState>();
let config = state.config.write().unwrap();
let $name = config.$prop;
}
}
pub(crate) use get_state;

View File

@ -1,28 +1,172 @@
use core::time::Duration;
use std::io; use std::io;
use std::net::SocketAddrV4; use std::net::{SocketAddr, SocketAddrV4};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::time::sleep;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use crate::clientinfo; use crate::{clientinfo, clientinfo::Client};
use crate::errors::RequestError; use crate::errors::*;
use crate::ipc::{Request, Approval}; 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 {
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);
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);
}
async fn try_handle(&mut self) -> Result<(), RequestError> {
let _ = self.recv_request().await?;
let clients = self.get_clients()?;
if self.includes_banned(&clients) {
self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
return Ok(())
}
let req = Request {id: self.request_id, clients};
self.app.emit_all("credentials-request", &req)?;
let starting_visibility = self.show_window()?;
match self.wait_for_response().await? {
Approval::Approved => self.send_credentials().await?,
Approval::Denied => {
let state = self.app.state::<AppState>();
for client in req.clients {
state.add_ban(client, self.app.clone());
}
}
}
// 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().unwrap();
Duration::from_millis(config.rehide_ms)
};
sleep(delay).await;
if !starting_visibility && state.req_count() == 0 {
let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
window.hide()?;
}
Ok(())
}
async fn recv_request(&mut self) -> Result<Vec<u8>, RequestError> {
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(RequestError::RequestTooLarge);}
}
if cfg!(debug_assertions) {
println!("{}", std::str::from_utf8(&buf).unwrap());
}
Ok(buf)
}
fn get_clients(&self) -> Result<Vec<Option<Client>>, RequestError> {
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())?;
Ok(clients)
}
fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
let state = self.app.state::<AppState>();
clients.iter().any(|c| state.is_banned(c))
}
fn show_window(&self) -> Result<bool, RequestError> {
let window = self.app.get_window("main").ok_or(RequestError::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, RequestError> {
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_credentials(&mut self) -> Result<(), RequestError> {
let state = self.app.state::<AppState>();
let creds = state.get_creds_serialized()?;
self.stream.write(b"\r\nContent-Length: ").await?;
self.stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
self.stream.write(b"\r\n\r\n").await?;
self.stream.write(creds.as_bytes()).await?;
self.stream.write(b"\r\n\r\n").await?;
Ok(())
}
}
pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> { pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> {
let listener = TcpListener::bind(&addr).await?; let listener = TcpListener::bind(&addr).await?;
println!("Listening on {addr}"); println!("Listening on {addr}");
loop { loop {
let new_handle = app_handle.app_handle();
match listener.accept().await { match listener.accept().await {
Ok((stream, _)) => { Ok((stream, _)) => {
tokio::spawn(async { let handler = Handler::new(stream, app_handle.app_handle());
if let Err(e) = handle(stream, new_handle).await { tauri::async_runtime::spawn(handler.handle());
eprintln!("{e}");
}
});
}, },
Err(e) => { Err(e) => {
eprintln!("Error accepting connection: {e}"); eprintln!("Error accepting connection: {e}");
@ -30,68 +174,3 @@ pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()>
} }
} }
} }
// it doesn't really return Approval, we just need to placate the compiler
async fn stall(stream: &mut TcpStream) -> Result<Approval, tokio::io::Error> {
let delay = std::time::Duration::from_secs(1);
loop {
tokio::time::sleep(delay).await;
stream.write(b"x").await?;
}
}
async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), RequestError> {
let (chan_send, chan_recv) = oneshot::channel();
let app_state = app_handle.state::<crate::state::AppState>();
let request_id = app_state.register_request(chan_send);
let peer_addr = match stream.peer_addr()? {
std::net::SocketAddr::V4(addr) => addr,
_ => unreachable!(), // we only listen on IPv4
};
let clients = clientinfo::get_clients(peer_addr.port())?;
// Do we want to panic if this fails? Does that mean the frontend is dead?
let req = Request {id: request_id, clients};
app_handle.emit_all("credentials-request", req).unwrap();
let mut buf = [0; 8192]; // it's what tokio's BufReader uses
let mut n = 0;
loop {
n += stream.read(&mut buf[n..]).await?;
if &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
if n == buf.len() {return Err(RequestError::RequestTooLarge);}
}
println!("{}", std::str::from_utf8(&buf).unwrap());
stream.write(b"HTTP/1.0 200 OK\r\n").await?;
stream.write(b"Content-Type: application/json\r\n").await?;
stream.write(b"X-Creddy-delaying-tactic: ").await?;
let approval = tokio::select!{
e = stall(&mut stream) => e?, // this will never return Ok, just Err if it can't write to the stream
r = chan_recv => r.unwrap(), // only panics if the sender is dropped without sending, which shouldn't happen
};
if matches!(approval, Approval::Denied) {
// because we own the stream, it gets closed when we return.
// Unfortunately we've already signaled 200 OK, there's no way around this -
// we have to write the status code first thing, and we have to assume that the user
// might need more time than that gives us (especially if entering the passphrase).
// Fortunately most AWS libs automatically retry if the request dies uncompleted, allowing
// us to respond with a proper error status.
return Ok(());
}
let creds = app_state.get_creds_serialized()?;
stream.write(b"\r\nContent-Length: ").await?;
stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
stream.write(b"\r\n\r\n").await?;
stream.write(creds.as_bytes()).await?;
stream.write(b"\r\n\r\n").await?;
Ok(())
}

View File

@ -1,8 +1,10 @@
use std::collections::HashMap; use core::time::Duration;
use std::collections::{HashMap, HashSet};
use std::sync::RwLock; use std::sync::RwLock;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
use tokio::time::sleep;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions}; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions};
use sodiumoxide::crypto::{ use sodiumoxide::crypto::{
pwhash, pwhash,
@ -11,12 +13,15 @@ use sodiumoxide::crypto::{
secretbox::{Nonce, Key} secretbox::{Nonce, Key}
}; };
use tauri::async_runtime as runtime; use tauri::async_runtime as runtime;
use tauri::Manager;
use crate::{config, config::AppConfig};
use crate::ipc; use crate::ipc;
use crate::clientinfo::Client;
use crate::errors::*; use crate::errors::*;
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum Credentials { pub enum Credentials {
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
@ -34,6 +39,7 @@ pub enum Credentials {
} }
#[derive(Debug)]
pub struct LockedCredentials { pub struct LockedCredentials {
access_key_id: String, access_key_id: String,
secret_key_enc: Vec<u8>, secret_key_enc: Vec<u8>,
@ -42,6 +48,7 @@ pub struct LockedCredentials {
} }
#[derive(Debug)]
pub enum Session { pub enum Session {
Unlocked(Credentials), Unlocked(Credentials),
Locked(LockedCredentials), Locked(LockedCredentials),
@ -49,36 +56,34 @@ pub enum Session {
} }
// #[derive(Serialize, Deserialize)] #[derive(Debug)]
// pub enum SessionStatus {
// Unlocked,
// Locked,
// Empty,
// }
pub struct AppState { pub struct AppState {
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 open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
pool: SqlitePool, pool: SqlitePool,
} }
impl AppState { impl AppState {
pub fn new() -> Result<Self, SetupError> { pub async fn load() -> Result<Self, SetupError> {
let conn_opts = SqliteConnectOptions::new() let conn_opts = SqliteConnectOptions::new()
.filename("creddy.db") .filename(config::get_or_create_db_path())
.create_if_missing(true); .create_if_missing(true);
let pool_opts = SqlitePoolOptions::new(); let pool_opts = SqlitePoolOptions::new();
let pool: SqlitePool = runtime::block_on(pool_opts.connect_with(conn_opts))?; let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
runtime::block_on(sqlx::migrate!().run(&pool))?; sqlx::migrate!().run(&pool).await?;
let creds = runtime::block_on(Self::load_creds(&pool))?; let creds = Self::load_creds(&pool).await?;
let conf = AppConfig::load(&pool).await?;
let state = AppState { let state = AppState {
config: RwLock::new(conf),
session: RwLock::new(creds), session: RwLock::new(creds),
request_count: RwLock::new(0), request_count: RwLock::new(0),
open_requests: RwLock::new(HashMap::new()), open_requests: RwLock::new(HashMap::new()),
bans: RwLock::new(HashSet::new()),
pool, pool,
}; };
@ -86,7 +91,7 @@ impl AppState {
} }
async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> { async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> {
let res = sqlx::query!("SELECT * FROM credentials") let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
let row = match res { let row = match res {
@ -110,13 +115,17 @@ impl AppState {
Ok(Session::Locked(creds)) Ok(Session::Locked(creds))
} }
pub async fn save_creds(&self, creds: Credentials, passphrase: &str) -> Result<(), sqlx::error::Error> { pub async fn save_creds(&self, creds: Credentials, passphrase: &str) -> Result<(), UnlockError> {
let (key_id, secret_key) = match creds { let (key_id, secret_key) = match creds {
Credentials::LongLived {access_key_id, secret_access_key} => { Credentials::LongLived {access_key_id, secret_access_key} => {
(access_key_id, secret_access_key) (access_key_id, secret_access_key)
}, },
_ => unreachable!(), _ => unreachable!(),
}; };
// do this first so that if it fails we don't save bad credentials
self.new_session(&key_id, &secret_key).await?;
let salt = pwhash::gen_salt(); let salt = pwhash::gen_salt();
let mut key_buf = [0; secretbox::KEYBYTES]; let mut key_buf = [0; secretbox::KEYBYTES];
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap(); pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap();
@ -124,16 +133,31 @@ impl AppState {
// not sure we need both salt AND nonce given that we generate a // not sure we need both salt AND nonce given that we generate a
// fresh salt every time we encrypt, but better safe than sorry // fresh salt every time we encrypt, but better safe than sorry
let nonce = secretbox::gen_nonce(); let nonce = secretbox::gen_nonce();
let key_enc = secretbox::seal(secret_key.as_bytes(), &nonce, &key); let secret_key_enc = secretbox::seal(secret_key.as_bytes(), &nonce, &key);
// insert into database
// eventually replace this with a temporary session sqlx::query(
let mut session = self.session.write().unwrap(); "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
*session = Session::Unlocked(Credentials::LongLived { VALUES (?, ?, ?, ?, strftime('%s'))"
access_key_id: key_id, )
secret_access_key: secret_key, .bind(&key_id)
}); .bind(&secret_key_enc)
.bind(&salt.0[0..])
.bind(&nonce.0[0..])
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
new_config.save(&self.pool).await?;
let mut live_config = self.config.write().unwrap();
if new_config.start_on_login != live_config.start_on_login {
config::set_auto_launch(new_config.start_on_login)?;
}
*live_config = new_config;
Ok(()) Ok(())
} }
@ -150,6 +174,16 @@ impl AppState {
*count *count
} }
pub fn unregister_request(&self, id: u64) {
let mut open_requests = self.open_requests.write().unwrap();
open_requests.remove(&id);
}
pub fn req_count(&self) -> usize {
let open_requests = self.open_requests.read().unwrap();
open_requests.len()
}
pub fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { pub fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
let mut open_requests = self.open_requests.write().unwrap(); let mut open_requests = self.open_requests.write().unwrap();
let chan = open_requests let chan = open_requests
@ -161,27 +195,44 @@ impl AppState {
.map_err(|_e| SendResponseError::Abandoned) .map_err(|_e| SendResponseError::Abandoned)
} }
pub fn add_ban(&self, client: Option<Client>, app: tauri::AppHandle) {
let mut bans = self.bans.write().unwrap();
bans.insert(client.clone());
runtime::spawn(async move {
sleep(Duration::from_secs(5)).await;
let state = app.state::<AppState>();
let mut bans = state.bans.write().unwrap();
bans.remove(&client);
});
}
pub fn is_banned(&self, client: &Option<Client>) -> bool {
self.bans.read().unwrap().contains(&client)
}
pub async fn decrypt(&self, passphrase: &str) -> Result<(), UnlockError> { pub async fn decrypt(&self, passphrase: &str) -> Result<(), UnlockError> {
let session = self.session.read().unwrap(); let (key_id, secret) = {
let locked = match *session { // do this all in a block so that we aren't holding a lock across an await
Session::Empty => {return Err(UnlockError::NoCredentials);}, let session = self.session.read().unwrap();
Session::Unlocked(_) => {return Err(UnlockError::NotLocked);}, let locked = match *session {
Session::Locked(ref c) => c, Session::Empty => {return Err(UnlockError::NoCredentials);},
Session::Unlocked(_) => {return Err(UnlockError::NotLocked);},
Session::Locked(ref c) => c,
};
let mut key_buf = [0; secretbox::KEYBYTES];
// pretty sure this only fails if we're out of memory
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &locked.salt).unwrap();
let decrypted = secretbox::open(&locked.secret_key_enc, &locked.nonce, &Key(key_buf))
.map_err(|_e| UnlockError::BadPassphrase)?;
let secret_str = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
(locked.access_key_id.clone(), secret_str)
}; };
let mut key_buf = [0; secretbox::KEYBYTES]; self.new_session(&key_id, &secret).await?;
// pretty sure this only fails if we're out of memory
pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &locked.salt).unwrap();
let decrypted = secretbox::open(&locked.secret_key_enc, &locked.nonce, &Key(key_buf))
.map_err(|_e| UnlockError::BadPassphrase)?;
let secret_str = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
let mut session = self.session.write().unwrap();
let creds = Credentials::LongLived {
access_key_id: locked.access_key_id.clone(),
secret_access_key: secret_str,
};
*session = Session::Unlocked(creds);
Ok(()) Ok(())
} }
@ -193,4 +244,56 @@ impl AppState {
Session::Empty => Err(GetCredentialsError::Empty), Session::Empty => Err(GetCredentialsError::Empty),
} }
} }
async fn new_session(&self, key_id: &str, secret_key: &str) -> Result<(), GetSessionError> {
let creds = aws_sdk_sts::Credentials::new(
key_id,
secret_key,
None, // token
None, // expiration
"creddy", // "provider name" apparently
);
let config = aws_config::from_env()
.credentials_provider(creds)
.load()
.await;
let client = aws_sdk_sts::Client::new(&config);
let resp = client.get_session_token()
.duration_seconds(43_200)
.send()
.await?;
let aws_session = resp.credentials().ok_or(GetSessionError::NoCredentials)?;
let access_key_id = aws_session.access_key_id()
.ok_or(GetSessionError::NoCredentials)?
.to_string();
let secret_access_key = aws_session.secret_access_key()
.ok_or(GetSessionError::NoCredentials)?
.to_string();
let token = aws_session.session_token()
.ok_or(GetSessionError::NoCredentials)?
.to_string();
let expiration = aws_session.expiration()
.ok_or(GetSessionError::NoCredentials)?
.fmt(aws_smithy_types::date_time::Format::DateTime)
.unwrap(); // only fails if the d/t is out of range, which it can't be for this format
let mut app_session = self.session.write().unwrap();
let session_creds = Credentials::ShortLived {
access_key_id,
secret_access_key,
token,
expiration,
};
if cfg!(debug_assertions) {
println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
}
*app_session = Session::Unlocked(session_creds);
Ok(())
}
} }

View File

@ -1,42 +0,0 @@
use sodiumoxide::crypto::{pwhash, secretbox};
use crate::state;
pub fn save(data: &str, passphrase: &str) {
let salt = pwhash::Salt([0; 32]); // yes yes, just for now
let mut kbuf = [0; secretbox::KEYBYTES];
pwhash::derive_key_interactive(&mut kbuf, passphrase.as_bytes(), &salt)
.expect("Couldn't compute password hash. Are you out of memory?");
let key = secretbox::Key(kbuf);
let nonce = secretbox::Nonce([0; 24]); // we don't care about e.g. replay attacks so this might be safe?
let encrypted = secretbox::seal(data.as_bytes(), &nonce, &key);
//todo: store in a database, along with salt, nonce, and hash parameters
std::fs::write("credentials.enc", &encrypted).expect("Failed to write file.");
//todo: key is automatically zeroed, but we should use 'zeroize' or something to zero out passphrase and data
}
// pub fn load(passphrase: &str) -> String {
// let salt = pwhash::Salt([0; 32]);
// let mut kbuf = [0; secretbox::KEYBYTES];
// pwhash::derive_key_interactive(&mut kbuf, passphrase.as_bytes(), &salt)
// .expect("Couldn't compute password hash. Are you out of memory?");
// let key = secretbox::Key(kbuf);
// let nonce = secretbox::Nonce([0; 24]);
// let encrypted = std::fs::read("credentials.enc").expect("Failed to read file.");
// let decrypted = secretbox::open(&encrypted, &nonce, &key).expect("Failed to decrypt.");
// String::from_utf8(decrypted).expect("Invalid utf-8")
// }
pub fn load(passphrase: &str) -> state::Credentials {
state::Credentials::ShortLived {
access_key_id: "ASIAZ7WSVLORKQI27QGB".to_string(),
secret_access_key: "blah".to_string(),
token: "gah".to_string(),
expiration: "2022-11-29T10:45:12Z".to_string(),
}
}

36
src-tauri/src/tray.rs Normal file
View File

@ -0,0 +1,36 @@
use tauri::{
AppHandle,
Manager,
SystemTray,
SystemTrayEvent,
SystemTrayMenu,
CustomMenuItem,
};
pub fn create() -> SystemTray {
let show = CustomMenuItem::new("show".to_string(), "Show");
let quit = CustomMenuItem::new("exit".to_string(), "Exit");
let menu = SystemTrayMenu::new()
.add_item(show)
.add_item(quit);
SystemTray::new().with_menu(menu)
}
pub fn handle_event(app: &AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::MenuItemClick{ id, .. } => {
match id.as_str() {
"exit" => app.exit(0),
"show" => {
let _ = app.get_window("main").map(|w| w.show());
}
_ => (),
}
}
_ => (),
}
}

View File

@ -29,7 +29,7 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"identifier": "com.tauri.dev", "identifier": "creddy",
"longDescription": "", "longDescription": "",
"macOS": { "macOS": {
"entitlements": null, "entitlements": null,
@ -58,9 +58,15 @@
"fullscreen": false, "fullscreen": false,
"height": 600, "height": 600,
"resizable": true, "resizable": true,
"label": "main",
"title": "Creddy", "title": "Creddy",
"width": 800 "width": 800,
"visible": false
} }
] ],
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
}
} }
} }

View File

@ -1,37 +1,20 @@
<script> <script>
import { emit, listen } from '@tauri-apps/api/event'; import { emit, listen } from '@tauri-apps/api/event';
import queue from './lib/queue.js';
const VIEWS = import.meta.glob('./views/*.svelte', {eager: true});
window.emit = emit;
window.queue = queue;
var appState = {
currentRequest: null,
pendingRequests: queue(),
credentialStatus: 'locked',
}
window.appState = appState;
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
window.invoke = invoke;
import { appState } from './lib/state.js';
import { views, currentView, navigate } from './lib/routing.js';
var currentView = VIEWS['./views/Home.svelte'].default; $views = import.meta.glob('./views/*.svelte', {eager: true});
window.currentView = currentView; navigate('Home');
window.VIEWS = VIEWS;
function navigate(svelteEvent) { invoke('get_config').then(config => $appState.config = config);
const moduleName = `./views/${svelteEvent.detail.target}.svelte`;
currentView = VIEWS[moduleName].default;
}
window.navigate = navigate;
listen('credentials-request', (tauriEvent) => { listen('credentials-request', (tauriEvent) => {
appState.pendingRequests.put(tauriEvent.payload); $appState.pendingRequests.put(tauriEvent.payload);
console.log('Received request.');
}); });
</script> </script>
<svelte:component this={currentView} on:navigate={navigate} bind:appState={appState} />
<svelte:component this="{$currentView}" />

8
src/lib/errors.js Normal file
View File

@ -0,0 +1,8 @@
export function getRootCause(error) {
if (error.source) {
return getRootCause(error.source);
}
else {
return error;
}
}

10
src/lib/routing.js Normal file
View File

@ -0,0 +1,10 @@
import { writable, get } from 'svelte/store';
export let views = writable();
export let currentView = writable();
export function navigate(viewName) {
let v = get(views)[`./views/${viewName}.svelte`].default;
currentView.set(v)
}

9
src/lib/state.js Normal file
View File

@ -0,0 +1,9 @@
import { writable } from 'svelte/store';
import queue from './queue.js';
export let appState = writable({
currentRequest: null,
pendingRequests: queue(),
credentialStatus: 'locked',
});

9
src/ui/Button.svelte Normal file
View File

@ -0,0 +1,9 @@
<script>
import Icon from './Icon.svelte';
export let icon = null;
</script>
<button>
{#if icon}<Icon name={icon} class="w-4 text-gray-200" />{/if}
<slot></slot>
</button>

67
src/ui/ErrorAlert.svelte Normal file
View File

@ -0,0 +1,67 @@
<script>
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
let extraClasses;
export {extraClasses as class};
export let slideDuration = 150;
let animationClass = "";
export function shake() {
animationClass = 'shake';
window.setTimeout(() => animationClass = "", 400);
}
</script>
<style>
/* animation from https://svelte.dev/repl/e606c27c864045e5a9700691a7417f99?version=3.58.0 */
@keyframes shake {
0% {
transform: translateX(0px);
}
20% {
transform: translateX(10px);
}
40% {
transform: translateX(-10px);
}
60% {
transform: translateX(5px);
}
80% {
transform: translateX(-5px);
}
90% {
transform: translateX(2px);
}
95% {
transform: translateX(-2px);
}
100% {
transform: translateX(0px);
}
}
.shake {
animation-name: shake;
animation-play-state: running;
animation-duration: 0.4s;
}
</style>
<div in:slide="{{duration: slideDuration}}" class="alert alert-error shadow-lg {animationClass} {extraClasses}">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>
<slot></slot>
</span>
</div>
{#if $$slots.buttons}
<div>
<slot name="buttons"></slot>
</div>
{/if}
</div>

11
src/ui/Icon.svelte Normal file
View File

@ -0,0 +1,11 @@
<script>
const ICONS = import.meta.glob('./icons/*.svelte', {eager: true});
export let name;
let classes = "";
export {classes as class};
let svg = ICONS[`./icons/${name}.svelte`].default;
</script>
<svelte:component this={svg} class={classes} />

43
src/ui/Link.svelte Normal file
View File

@ -0,0 +1,43 @@
<script>
import { navigate } from '../lib/routing.js';
export let target;
export let hotkey = null;
export let ctrl = false
export let alt = false;
export let shift = false;
function click() {
if (typeof target === 'string') {
navigate(target);
}
else if (typeof target === 'function') {
target();
}
else {
throw(`Link target is not a string or a function: ${target}`)
}
}
function handleHotkey(event) {
if (!hotkey) return;
if (ctrl && !event.ctrlKey) return;
if (alt && !event.altKey) return;
if (shift && !event.shiftKey) return;
if (event.code === hotkey) {
click();
}
else if (hotkey === 'Enter' && event.code === 'NumpadEnter') {
click();
}
}
</script>
<svelte:window on:keydown={handleHotkey} />
<a href="#" on:click="{click}">
<slot></slot>
</a>

23
src/ui/Nav.svelte Normal file
View File

@ -0,0 +1,23 @@
<script>
import Link from './Link.svelte';
import Icon from './Icon.svelte';
</script>
<nav class="fixed top-0 grid grid-cols-2 w-full p-2">
<div>
<Link target="Home">
<button class="btn btn-square btn-ghost align-middle">
<Icon name="home" class="w-8 h-8 stroke-2" />
</button>
</Link>
</div>
<div class="justify-self-end">
<Link target="Settings">
<button class="btn btn-square btn-ghost align-middle ">
<Icon name="cog-8-tooth" class="w-8 h-8 stroke-2" />
</button>
</Link>
</div>
</nav>

View File

@ -0,0 +1,8 @@
<script>
let classes = "";
export {classes as class};
</script>
<svg class={classes} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>

View File

@ -0,0 +1,9 @@
<script>
let classes = "";
export {classes as class};
</script>
<svg class="w-6 h-6 {classes}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

8
src/ui/icons/home.svelte Normal file
View File

@ -0,0 +1,8 @@
<script>
let classes = "";
export {classes as class};
</script>
<svg class="w-6 h-6 {classes}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>

View File

@ -0,0 +1,8 @@
<script>
let classes = "";
export {classes as class};
</script>
<svg class={classes} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>

View File

@ -0,0 +1,62 @@
<script>
import { createEventDispatcher } from 'svelte';
import Setting from './Setting.svelte';
export let title;
export let value;
export let unit = '';
export let min = null;
export let max = null;
export let decimal = false;
let error = null;
let localValue = value.toString();
const dispatch = createEventDispatcher();
function validate(event) {
localValue = localValue.replace(/[^-0-9.]/g, '');
// Don't update the value, but also don't error, if it's empty
// or if it could be the start of a negative or decimal number
if (localValue.match(/^$|^-$|^\.$/) !== null) {
error = null;
return;
}
let num = parseFloat(localValue);
if (num % 1 !== 0 && !decimal) {
error = `${num} is not a whole number`;
}
else if (min !== null && num < min) {
error = `Too low (minimum ${min})`;
}
else if (max !== null && num > max) {
error = `Too large (maximum ${max})`
}
else {
error = null;
value = num;
dispatch('update', {value})
}
}
</script>
<Setting {title}>
<div slot="input">
{#if unit}
<span class="mr-2">{unit}:</span>
{/if}
<div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip="{error}">
<input
type="text"
class="input input-sm input-bordered text-right"
size="{Math.max(5, localValue.length)}"
class:input-error={error}
bind:value={localValue}
on:input="{validate}"
/>
</div>
</div>
<slot name="description" slot="description"></slot>
</Setting>

View File

@ -0,0 +1,19 @@
<script>
import { slide } from 'svelte/transition';
import ErrorAlert from '../ErrorAlert.svelte';
export let title;
</script>
<div class="divider"></div>
<div class="flex justify-between">
<h3 class="text-lg font-bold">{title}</h3>
<slot name="input"></slot>
</div>
{#if $$slots.description}
<p class="mt-3">
<slot name="description"></slot>
</p>
{/if}

View 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}>
<input
slot="input"
type="checkbox"
class="toggle toggle-success"
bind:checked={value}
on:change={e => dispatch('update', {value: e.target.checked})}
/>
<slot name="description" slot="description"></slot>
</Setting>

3
src/ui/settings/index.js Normal file
View File

@ -0,0 +1,3 @@
export { default as Setting } from './Setting.svelte';
export { default as ToggleSetting } from './ToggleSetting.svelte';
export { default as NumericSetting } from './NumericSetting.svelte';

View File

@ -1,39 +1,59 @@
<script> <script>
import { createEventDispatcher } from 'svelte';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
export let appState; import { navigate } from '../lib/routing.js';
import { appState } from '../lib/state.js';
import Link from '../ui/Link.svelte';
import Icon from '../ui/Icon.svelte';
const dispatch = createEventDispatcher();
async function approve() { async function approve() {
let status = await invoke('get_session_status'); let status = await invoke('get_session_status');
if (status === 'unlocked') { if (status === 'unlocked') {
dispatch('navigate', {target: 'ShowApproved'}); navigate('ShowApproved');
} }
else if (status === 'locked') { else if (status === 'locked') {
dispatch('navigate', {target: 'Unlock'}) navigate('Unlock');
} }
else { else {
dispatch('navigate', {target: 'EnterCredentials'}); navigate('EnterCredentials');
} }
} }
function deny() { var appName = null;
dispatch('navigate', {target: 'ShowDenied'}); if ($appState.currentRequest.clients.length === 1) {
let path = $appState.currentRequest.clients[0].exe;
let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
appName = m[1] || m[2];
} }
</script> </script>
<h2 class="text-3xl text-gray-200">An application would like to access your AWS credentials.</h2>
<button on:click={approve}> <div class="flex flex-col space-y-4 p-4 m-auto max-w-max h-screen justify-center">
<svg class="w-32 stroke-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"> <!-- <div class="p-4 rounded-box border-2 border-neutral-content"> -->
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <div class="space-y-1 mb-4">
</svg> <h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
</button> {#each $appState.currentRequest.clients as client}
<p>Path: {client ? client.exe : 'Unknown'}</p>
<p>PID: {client ? client.pid : 'Unknown'}</p>
{/each}
</div>
<button on:click={deny}> <div class="grid grid-cols-2">
<svg class="w-32 stroke-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"> <Link target="ShowDenied" hotkey="Escape">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> <button class="btn btn-error justify-self-start">
</svg> Deny
</button> <kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
</button>
</Link>
<Link target="{approve}" hotkey="Enter" shift="{true}">
<button class="btn btn-success justify-self-end">
Approve
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
<span class="mx-0.5">+</span>
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">Enter</kbd>
</button>
</Link>
</div>
</div>

View File

@ -1,41 +1,72 @@
<script> <script>
import { onMount, createEventDispatcher } from 'svelte'; import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import { getRootCause } from '../lib/errors.js';
export let appState; import { appState } from '../lib/state.js';
import { navigate } from '../lib/routing.js';
import Link from '../ui/Link.svelte';
import ErrorAlert from '../ui/ErrorAlert.svelte';
const dispatch = createEventDispatcher();
let AccessKeyId, SecretAccessKey, passphrase let errorMsg = null;
let alert;
let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
function confirm() {
if (passphrase !== confirmPassphrase) {
errorMsg = 'Passphrases do not match.'
}
}
async function save() { async function save() {
if (passphrase !== confirmPassphrase) {
alert.shake();
return;
}
let credentials = {AccessKeyId, SecretAccessKey};
try { try {
console.log('Saving credentials.');
let credentials = {AccessKeyId, SecretAccessKey};
console.log(credentials);
await invoke('save_credentials', {credentials, passphrase}); await invoke('save_credentials', {credentials, passphrase});
if (appState.currentRequest) { if ($appState.currentRequest) {
dispatch('navigate', {target: 'ShowApproved'}) navigate('ShowApproved');
} }
else { else {
dispatch('navigate', {target: 'Home'}) navigate('Home');
} }
} }
catch (e) { catch (e) {
console.log("Error saving credentials:", e); if (e.code === "GetSession") {
let root = getRootCause(e);
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
}
else {
errorMsg = e.msg;
}
if (alert) {
alert.shake();
}
} }
} }
</script> </script>
<form action="#" on:submit|preventDefault="{save}">
<div class="text-gray-200">AWS Access Key ID</div>
<input class="text-gray-200 bg-zinc-800" type="text" bind:value="{AccessKeyId}" />
<div class="text-gray-200">AWS Secret Access Key</div> <form action="#" on:submit|preventDefault="{save}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
<input class="text-gray-200 bg-zinc-800" type="text" bind:value="{SecretAccessKey}" /> <h2 class="text-2xl font-bold text-center">Enter your credentials</h2>
<div class="text-gray-200">Passphrase</div> {#if errorMsg}
<input class="text-gray-200 bg-zinc-800" type="text" bind:value="{passphrase}" /> <ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
{/if}
<input class="text-gray-200" type="submit" /> <input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
<input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" class="input input-bordered" />
<input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
<input type="submit" class="btn btn-primary" />
<Link target="Home" hotkey="Escape">
<button class="btn btn-sm btn-outline w-full">Cancel</button>
</Link>
</form> </form>

View File

@ -1,18 +1,42 @@
<script> <script>
import { onMount, createEventDispatcher } from 'svelte'; import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/tauri';
export let appState; import { appState } from '../lib/state.js';
import { navigate } from '../lib/routing.js';
import Nav from '../ui/Nav.svelte';
import Icon from '../ui/Icon.svelte';
import Link from '../ui/Link.svelte';
const dispatch = createEventDispatcher();
onMount(async () => { onMount(async () => {
// will block until a request comes in // will block until a request comes in
let req = await appState.pendingRequests.get(); let req = await $appState.pendingRequests.get();
appState.currentRequest = req; $appState.currentRequest = req;
console.log('Got credentials request from queue:'); navigate('Approve');
console.log(req);
dispatch('navigate', {target: 'Approve'});
}); });
</script> </script>
<h1 class="text-4xl text-gray-300">Creddy</h1>
<Nav />
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
{#await invoke('get_session_status') then status}
{#if status === 'locked'}
<img src="/static/padlock-closed.svg" alt="A locked padlock" class="w-32" />
<h2 class="text-2xl font-bold">Creddy is locked</h2>
<Link target="Unlock">
<button class="btn btn-primary">Unlock</button>
</Link>
{:else if status === 'unlocked'}
<img src="/static/padlock-open.svg" alt="An unlocked padlock" class="w-24" />
<h2 class="text-2xl font-bold">Waiting for requests</h2>
{:else if status === 'empty'}
<Link target="EnterCredentials">
<button class="btn btn-primary">Enter Credentials</button>
</Link>
{/if}
{/await}
</div>

59
src/views/Settings.svelte Normal file
View File

@ -0,0 +1,59 @@
<script>
import { invoke } from '@tauri-apps/api/tauri';
import { appState } from '../lib/state.js';
import Nav from '../ui/Nav.svelte';
import Link from '../ui/Link.svelte';
import ErrorAlert from '../ui/ErrorAlert.svelte';
// import Setting from '../ui/settings/Setting.svelte';
import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
async function save() {
await invoke('save_config', {config: $appState.config});
}
</script>
<Nav />
{#await invoke('get_config') then config}
<div class="max-w-md mx-auto mt-1.5 p-4">
<h2 class="text-2xl font-bold text-center">Settings</h2>
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
<svelte:fragment slot="description">
Start Creddy when you log in to your computer.
</svelte:fragment>
</ToggleSetting>
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
<svelte:fragment slot="description">
Minimize to the system tray at startup.
</svelte:fragment>
</ToggleSetting>
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
<svelte:fragment slot="description">
How long to wait after a request is approved/denied before minimizing
the window to tray. Only applicable if the window was minimized
to tray before the request was received.
</svelte:fragment>
</NumericSetting>
<NumericSetting title="Listen port" bind:value={$appState.config.listen_port} min=1 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">
<Link slot="input" target="EnterCredentials">
<button class="btn btn-sm btn-primary">Update</button>
</Link>
<svelte:fragment slot="description">
Update or re-enter your encrypted credentials.
</svelte:fragment>
</Setting>
</div>
{/await}

View File

@ -1,21 +1,78 @@
<script> <script>
import { onMount, createEventDispatcher } from 'svelte'; import { onMount } from 'svelte';
import { draw, fade } from 'svelte/transition';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
export let appState; import { appState } from '../lib/state.js';
import { navigate } from '../lib/routing.js';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Icon from '../ui/Icon.svelte';
import Link from '../ui/Link.svelte';
onMount(async () => { let success = false;
let error = null;
let drawDuration = $appState.config.rehide_ms >= 750 ? 500 : 0;
let fadeDuration = drawDuration * 0.6;
let fadeDelay = drawDuration * 0.4;
async function respond() {
let response = { let response = {
id: appState.currentRequest.id, id: $appState.currentRequest.id,
approval: 'Approved', approval: 'Approved',
} };
await invoke('respond', {response});
appState.currentRequest = null;
});
const dispatch = createEventDispatcher(); try {
window.setTimeout(() => dispatch('navigate', {target: 'Home'}), 3000); await invoke('respond', {response});
success = true;
$appState.currentRequest = null;
window.setTimeout(
() => navigate('Home'),
// Extra 50ms so the window can finish disappearing before the screen changes
Math.min(5000, $appState.config.rehide_ms + 50),
);
}
catch (e) {
error = e;
}
}
onMount(respond);
</script> </script>
<h1 class="text-4xl text-gray-300">Approved!</h1>
{#if error}
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
<ErrorAlert>
{error}
<svelte:fragment slot="buttons">
<Link target="Home">
<button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content">
Ok
</button>
</Link>
</svelte:fragment>
</ErrorAlert>
</div>
{:else if success}
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
<path in:draw="{{duration: drawDuration}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">Approved!</div>
</div>
{/if}
<!--
{#if error}
<div class="text-red-400">{error}</div>
{:else}
<h1 class="text-4xl text-gray-300">Approved!</h1>
{/if}
-->

View File

@ -1,21 +1,56 @@
<script> <script>
import { onMount, createEventDispatcher } from 'svelte'; import { onMount } from 'svelte';
import { draw, fade } from 'svelte/transition';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
export let appState; import { appState } from '../lib/state.js';
import { navigate } from '../lib/routing.js';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Icon from '../ui/Icon.svelte';
import Link from '../ui/Link.svelte';
onMount(async () => { let error = null;
async function respond() {
let response = { let response = {
id: appState.currentRequest.id, id: $appState.currentRequest.id,
approval: 'Denied', approval: 'Denied',
} }
await invoke('respond', {response});
appState.currentRequest = null;
});
const dispatch = createEventDispatcher(); try {
window.setTimeout(() => dispatch('navigate', {target: 'Home'}), 3000); await invoke('respond', {response});
$appState.currentRequest = null;
window.setTimeout(() => navigate('Home'), 1000);
}
catch (e) {
error = e;
}
}
onMount(respond);
</script> </script>
<h1 class="text-4xl text-gray-300">Denied!</h1> {#if error}
<div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
<ErrorAlert>
{error}
<svelte:fragment slot="buttons">
<Link target="Home">
<button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content" on:click="{() => navigate('Home')}">
Ok
</button>
</Link>
</svelte:fragment>
</ErrorAlert>
</div>
{:else}
<div class="flex flex-col items-center justify-center h-screen max-w-max m-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
<path in:draw="{{duration: 500}}" stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div in:fade="{{delay: 200, duration: 300}}" class="text-2xl font-bold">Denied!</div>
</div>
{/if}

View File

@ -1,25 +1,56 @@
<script> <script>
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import { createEventDispatcher } from 'svelte';
export let appState; import { appState } from '../lib/state.js';
import { navigate } from '../lib/routing.js';
import { getRootCause } from '../lib/errors.js';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Link from '../ui/Link.svelte';
const dispatch = createEventDispatcher();
let errorMsg = null;
let alert;
let passphrase = ''; let passphrase = '';
async function unlock() { async function unlock() {
console.log('invoking unlock command.')
try { try {
await invoke('unlock', {passphrase}); let r = await invoke('unlock', {passphrase});
$appState.credentialStatus = 'unlocked';
if ($appState.currentRequest) {
navigate('ShowApproved');
}
else {
navigate('Home');
}
} }
catch (e) { catch (e) {
console.log('Unlock error:', e); window.error = e;
if (e.code === 'GetSession') {
let root = getRootCause(e);
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
}
else {
errorMsg = e.msg;
}
if (alert) {
alert.shake();
}
} }
} }
</script> </script>
<form action="#" on:submit|preventDefault="{unlock}">
<div class="text-gray-200">Enter your passphrase:</div> <form action="#" on:submit|preventDefault="{unlock}" class="form-control space-y-4 max-w-sm m-auto p-4 h-screen justify-center">
<input class="text-gray-200 bg-zinc-800" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" /> <h2 class="font-bold text-2xl text-center">Enter your passphrase</h2>
{#if errorMsg}
<ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
{/if}
<input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
<input type="submit" class="btn btn-primary" />
<Link target="Home" hotkey="Escape">
<button class="btn btn-outline btn-sm w-full">Cancel</button>
</Link>
</form> </form>

430
static/padlock-closed.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

467
static/padlock-open.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -7,5 +7,7 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [
require('daisyui'),
],
} }