diff --git a/Cargo.lock b/Cargo.lock index b4c2857..e83d530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,8 +106,9 @@ dependencies = [ [[package]] name = "atuin" -version = "0.4.0" +version = "0.5.0" dependencies = [ + "base64 0.13.0", "chrono", "chrono-english", "cli-table", @@ -118,15 +119,21 @@ dependencies = [ "dotenv", "eyre", "fern", - "hostname", + "fork", "indicatif", "itertools", "log 0.4.14", + "parse_duration", + "rand 0.8.3", + "reqwest", + "rmp-serde", "rocket", "rocket_contrib", "rusqlite", + "rust-crypto", "serde 1.0.125", "serde_derive", + "serde_json", "shellexpand", "sodiumoxide", "structopt", @@ -134,6 +141,7 @@ dependencies = [ "tui", "unicode-width", "uuid", + "whoami", ] [[package]] @@ -223,6 +231,12 @@ dependencies = [ "serde 1.0.125", ] +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + [[package]] name = "byte-tools" version = "0.3.1" @@ -235,6 +249,12 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + [[package]] name = "cassowary" version = "0.3.0" @@ -268,6 +288,7 @@ dependencies = [ "libc", "num-integer", "num-traits 0.2.14", + "serde 1.0.125", "time", "winapi 0.3.9", ] @@ -316,9 +337,9 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efe942512e068e15991cbcef4e8182884555febbb21b5b4faf5dd5561850141a" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", ] [[package]] @@ -369,11 +390,27 @@ dependencies = [ "hkdf", "hmac", "percent-encoding 2.1.0", - "rand", + "rand 0.7.3", "sha2", "time", ] +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + [[package]] name = "crossbeam-utils" version = "0.8.1" @@ -469,9 +506,9 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", ] [[package]] @@ -552,6 +589,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "eyre" version = "0.6.5" @@ -601,6 +647,46 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fork" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c5b9b0bce249a456f83ac4404e8baad0d2ba81cf651949719a4f74eb7323bb" +dependencies = [ + "libc", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding 2.1.0", +] + [[package]] name = "fsevent" version = "0.4.0" @@ -620,6 +706,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -636,6 +728,60 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "futures-channel" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" + +[[package]] +name = "futures-io" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" + +[[package]] +name = "futures-sink" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" + +[[package]] +name = "futures-task" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" + +[[package]] +name = "futures-util" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.12.3" @@ -682,6 +828,25 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "h2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -739,14 +904,25 @@ dependencies = [ ] [[package]] -name = "hostname" -version = "0.3.1" +name = "http" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ - "libc", - "match_cfg", - "winapi 0.3.9", + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfb77c123b4e2f72a2069aeae0b4b4949cc7e966df277813fc16347e7549737" +dependencies = [ + "bytes", + "http", + "pin-project-lite", ] [[package]] @@ -755,6 +931,12 @@ version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + [[package]] name = "hyper" version = "0.10.16" @@ -765,13 +947,50 @@ dependencies = [ "httparse", "language-tags", "log 0.3.9", - "mime", + "mime 0.2.6", "num_cpus", "time", "traitobject", "typeable", "unicase", - "url", + "url 1.7.2", +] + +[[package]] +name = "hyper" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf09f61b52cfcf4c00de50df88ae423d6c02354e385a86341133b5338630ad1" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.5", + "native-tls", + "tokio", + "tokio-native-tls", ] [[package]] @@ -785,6 +1004,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indenter" version = "0.3.2" @@ -851,6 +1081,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + [[package]] name = "itertools" version = "0.10.0" @@ -866,6 +1102,15 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "js-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -968,12 +1213,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matches" version = "0.1.8" @@ -1002,9 +1241,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" dependencies = [ "migrations_internals", - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", ] [[package]] @@ -1016,6 +1255,12 @@ dependencies = [ "log 0.3.9", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "mio" version = "0.6.23" @@ -1029,12 +1274,25 @@ dependencies = [ "kernel32-sys", "libc", "log 0.4.14", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +dependencies = [ + "libc", + "log 0.4.14", + "miow 0.3.7", + "ntapi", + "winapi 0.3.9", +] + [[package]] name = "mio-extras" version = "2.0.6" @@ -1043,7 +1301,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" dependencies = [ "lazycell", "log 0.4.14", - "mio", + "mio 0.6.23", "slab", ] @@ -1059,6 +1317,33 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log 0.4.14", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.37" @@ -1093,12 +1378,56 @@ dependencies = [ "fsevent-sys", "inotify", "libc", - "mio", + "mio 0.6.23", "mio-extras", "walkdir", "winapi 0.3.9", ] +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.14", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits 0.2.14", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1109,6 +1438,29 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits 0.2.14", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -1161,6 +1513,39 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "openssl" +version = "0.10.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -1186,6 +1571,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parse_duration" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d" +dependencies = [ + "lazy_static", + "num", + "regex", +] + [[package]] name = "pear" version = "0.1.4" @@ -1220,6 +1616,38 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" +dependencies = [ + "proc-macro2 1.0.26", + "quote 1.0.9", + "syn 1.0.69", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.19" @@ -1258,9 +1686,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", "version_check 0.9.2", ] @@ -1270,7 +1698,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", "version_check 0.9.2", ] @@ -1286,9 +1714,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid 0.2.1", ] @@ -1308,7 +1736,7 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", ] [[package]] @@ -1322,6 +1750,29 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.7.3" @@ -1330,9 +1781,21 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.0", ] [[package]] @@ -1342,9 +1805,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -1354,13 +1842,40 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", +] + [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -1435,6 +1950,71 @@ version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf12057f289428dbf5c591c74bf10392e4a8003f993405a902f20117019022d4" +dependencies = [ + "base64 0.13.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper 0.14.5", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log 0.4.14", + "mime 0.3.16", + "native-tls", + "percent-encoding 2.1.0", + "pin-project-lite", + "serde 1.0.125", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url 2.2.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rmp" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f55e5fa1446c4d5dd1f5daeed2a4fe193071771a2636274d0d7a3b082aa7ad6" +dependencies = [ + "byteorder", + "num-traits 0.2.14", +] + +[[package]] +name = "rmp-serde" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839395ef53057db96b84c9238ab29e1a13f2e5c8ec9f66bef853ab4197303924" +dependencies = [ + "byteorder", + "rmp", + "serde 1.0.125", +] + [[package]] name = "rocket" version = "0.4.7" @@ -1506,7 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce364100ed7a1bf39257b69ebd014c1d5b4979b0d365d8c9ab0aa9c79645493d" dependencies = [ "cookie", - "hyper", + "hyper 0.10.16", "indexmap", "pear", "percent-encoding 1.0.1", @@ -1543,12 +2123,31 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + [[package]] name = "rust-ini" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + [[package]] name = "ryu" version = "1.0.5" @@ -1576,6 +2175,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.5" @@ -1591,6 +2200,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "0.8.23" @@ -1621,22 +2253,34 @@ version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", ] [[package]] name = "serde_json" -version = "1.0.62" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", "serde 1.0.125", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde 1.0.125", +] + [[package]] name = "sha2" version = "0.8.2" @@ -1670,6 +2314,16 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "sodiumoxide" version = "0.2.6" @@ -1718,9 +2372,9 @@ checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", - "syn 1.0.60", + "syn 1.0.69", ] [[package]] @@ -1748,15 +2402,29 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.60" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2 1.0.26", "quote 1.0.9", "unicode-xid 0.2.1", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall 0.2.4", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -1831,6 +2499,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio 0.7.11", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5143d049e85af7fbc36f5454d990e62c2df705b3589f123b71f441b6b59f443f" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log 0.4.14", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.4.10" @@ -1849,12 +2556,44 @@ dependencies = [ "serde 1.0.125", ] +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + [[package]] name = "traitobject" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "tui" version = "0.14.0" @@ -1947,11 +2686,23 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna", + "idna 0.1.5", "matches", "percent-encoding 1.0.1", ] +[[package]] +name = "url" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +dependencies = [ + "form_urlencoded", + "idna 0.2.2", + "matches", + "percent-encoding 2.1.0", +] + [[package]] name = "uuid" version = "0.8.2" @@ -1996,6 +2747,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log 0.4.14", + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2008,6 +2769,94 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" +dependencies = [ + "cfg-if 1.0.0", + "serde 1.0.125", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" +dependencies = [ + "bumpalo", + "lazy_static", + "log 0.4.14", + "proc-macro2 1.0.26", + "quote 1.0.9", + "syn 1.0.69", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" +dependencies = [ + "quote 1.0.9", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" +dependencies = [ + "proc-macro2 1.0.26", + "quote 1.0.9", + "syn 1.0.69", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" + +[[package]] +name = "web-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.2.8" @@ -2051,6 +2900,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a016024..9d33787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "atuin" -version = "0.4.0" +version = "0.5.0" authors = ["Ellie Huxtable "] edition = "2018" license = "MIT" @@ -9,20 +9,22 @@ description = "atuin - magical shell history" [dependencies] log = "0.4" fern = "0.6.0" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } eyre = "0.6" shellexpand = "2" structopt = "0.3" directories = "3" uuid = { version = "0.8", features = ["v4"] } indicatif = "0.15.0" -hostname = "0.3.1" +whoami = "1.1.2" rocket = "0.4.7" chrono-english = "0.1.4" cli-table = "0.4" config = "0.11" serde_derive = "1.0.125" serde = "1.0.125" +serde_json = "1.0.64" +rmp-serde = "0.15.4" tui = "0.14" termion = "1.5" unicode-width = "0.1" @@ -31,6 +33,12 @@ diesel = { version = "1.4.4", features = ["postgres", "chrono"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" sodiumoxide = "0.2.6" +reqwest = { version = "0.11", features = ["blocking", "json"] } +base64 = "0.13.0" +fork = "0.1.18" +parse_duration = "2.1.1" +rand = "0.8.3" +rust-crypto = "^0.2" [dependencies.rusqlite] version = "0.25" diff --git a/config.toml b/config.toml index 19a454e..9d5452c 100644 --- a/config.toml +++ b/config.toml @@ -3,36 +3,41 @@ # This section specifies the config for a local client, # ie where your shell history is on your local machine [local] -# (optional) -# where to store your database, default is your system data directory -# mac: ~/Library/Application Support/com.elliehuxtable.atuin/history.db -# linux: ~/.local/share/atuin/history.db -db_path = "~/.history.db" -# (optional, default us) -# date format used, either "us" or "uk" -dialect = "uk" -# (optional, default false) -# whether to enable sync of history. requires authentication -sync = false -# (optional, default 5m) -# how often to sync history. note that this is only triggered when a command is ran, and the last sync was >= this value ago -# set it to 0 to sync after every command -sync_frequency = "5m" -# (optional, default https://atuin.elliehuxtable.com) -# address of the sync server -sync_address = "https://atuin.elliehuxtable.com" +## where to store your database, default is your system data directory +## mac: ~/Library/Application Support/com.elliehuxtable.atuin/history.db +## linux: ~/.local/share/atuin/history.db +# db_path = "~/.history.db" + +## where to store your encryption key, default is your system data directory +# key_path = "~/.key" + +## where to store your auth session token, default is your system data directory +# session_path = "~/.key" + +## date format used, either "us" or "uk" +# dialect = "uk" + +## enable or disable automatic sync +# auto_sync = true + +## how often to sync history. note that this is only triggered when a command +## is ran, so sync intervals may well be longer +## set it to 0 to sync after every command +# sync_frequency = "5m" + +## address of the sync server +# sync_address = "https://api.atuin.sh" # This section configures the sync server, if you decide to host your own [remote] -# (optional, default 127.0.0.1) -# host to bind, can also be passed via CLI args -host = "127.0.0.1" -# (optional, default 8888) -# port to bind, can also be passed via CLI args -port = 8888 -# (optional, default false) -# whether to allow anyone to register an account -open_registration = false -# (required) -# URI for postgres (using development creds here) -db_uri="postgres://username:password@localhost/atuin" +## host to bind, can also be passed via CLI args +# host = "127.0.0.1" + +## port to bind, can also be passed via CLI args +# port = 8888 + +## whether to allow anyone to register an account +# open_registration = false + +## URI for postgres (using development creds here) +# db_uri="postgres://username:password@localhost/atuin" diff --git a/migrations/2021-03-20-151809_create_history/up.sql b/migrations/2021-03-20-151809_create_history/up.sql index 7cb19fc..4192b04 100644 --- a/migrations/2021-03-20-151809_create_history/up.sql +++ b/migrations/2021-03-20-151809_create_history/up.sql @@ -4,8 +4,10 @@ create table history ( id bigserial primary key, client_id text not null unique, -- the client-generated ID user_id bigserial not null, -- allow multiple users - mac varchar(128) not null, -- store a hashed mac address, to identify machines - more likely to be unique than hostname + hostname text not null, -- a unique identifier from the client (can be hashed, random, whatever) timestamp timestamp not null, -- one of the few non-encrypted metadatas - data varchar(8192) not null -- store the actual history data, encrypted. I don't wanna know! + data varchar(8192) not null, -- store the actual history data, encrypted. I don't wanna know! + + created_at timestamp not null default current_timestamp ); diff --git a/migrations/2021-03-20-171007_create_users/up.sql b/migrations/2021-03-20-171007_create_users/up.sql index 0eecea7..46c6a37 100644 --- a/migrations/2021-03-20-171007_create_users/up.sql +++ b/migrations/2021-03-20-171007_create_users/up.sql @@ -1,6 +1,11 @@ -- Your SQL goes here create table users ( id bigserial primary key, -- also store our own ID + username varchar(32) not null unique, -- being able to contact users is useful email varchar(128) not null unique, -- being able to contact users is useful password varchar(128) not null unique ); + +-- the prior index is case sensitive :( +CREATE UNIQUE INDEX email_unique_idx on users (LOWER(email)); +CREATE UNIQUE INDEX username_unique_idx on users (LOWER(username)); diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..9097740 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,36 @@ +use chrono::Utc; + +// This is shared between the client and the server, and has the data structures +// representing the requests/responses for each method. +// TODO: Properly define responses rather than using json! + +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterRequest { + pub email: String, + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddHistoryRequest { + pub id: String, + pub timestamp: chrono::DateTime, + pub data: String, + pub hostname: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CountResponse { + pub count: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListHistoryResponse { + pub history: Vec, +} diff --git a/src/command/history.rs b/src/command/history.rs index 05aed4b..3b4a717 100644 --- a/src/command/history.rs +++ b/src/command/history.rs @@ -1,10 +1,13 @@ use std::env; use eyre::Result; +use fork::{fork, Fork}; use structopt::StructOpt; use crate::local::database::Database; use crate::local::history::History; +use crate::local::sync; +use crate::settings::Settings; #[derive(StructOpt)] pub enum Cmd { @@ -50,21 +53,13 @@ fn print_list(h: &[History]) { } impl Cmd { - pub fn run(&self, db: &mut impl Database) -> Result<()> { + pub fn run(&self, settings: &Settings, db: &mut impl Database) -> Result<()> { match self { Self::Start { command: words } => { let command = words.join(" "); let cwd = env::current_dir()?.display().to_string(); - let h = History::new( - chrono::Utc::now().timestamp_nanos(), - command, - cwd, - -1, - -1, - None, - None, - ); + let h = History::new(chrono::Utc::now(), command, cwd, -1, -1, None, None); // print the ID // we use this as the key for calling end @@ -76,10 +71,23 @@ impl Cmd { Self::End { id, exit } => { let mut h = db.load(id)?; h.exit = *exit; - h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp; + h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos(); db.update(&h)?; + if settings.local.should_sync()? { + match fork() { + Ok(Fork::Parent(child)) => { + debug!("launched sync background process with PID {}", child); + } + Ok(Fork::Child) => { + debug!("running periodic background sync"); + sync::sync(settings, false, db)?; + } + Err(_) => println!("Fork failed"), + } + } + Ok(()) } diff --git a/src/command/login.rs b/src/command/login.rs new file mode 100644 index 0000000..4f58b77 --- /dev/null +++ b/src/command/login.rs @@ -0,0 +1,48 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::prelude::*; + +use eyre::Result; +use structopt::StructOpt; + +use crate::settings::Settings; + +#[derive(StructOpt)] +#[structopt(setting(structopt::clap::AppSettings::DeriveDisplayOrder))] +pub struct Cmd { + #[structopt(long, short)] + pub username: String, + + #[structopt(long, short)] + pub password: String, + + #[structopt(long, short, about = "the encryption key for your account")] + pub key: String, +} + +impl Cmd { + pub fn run(&self, settings: &Settings) -> Result<()> { + let mut map = HashMap::new(); + map.insert("username", self.username.clone()); + map.insert("password", self.password.clone()); + + let url = format!("{}/login", settings.local.sync_address); + let client = reqwest::blocking::Client::new(); + let resp = client.post(url).json(&map).send()?; + + let session = resp.json::>()?; + let session = session["session"].clone(); + + let session_path = settings.local.session_path.as_str(); + let mut file = File::create(session_path)?; + file.write_all(session.as_bytes())?; + + let key_path = settings.local.key_path.as_str(); + let mut file = File::create(key_path)?; + file.write_all(&base64::decode(self.key.clone())?)?; + + println!("Logged in!"); + + Ok(()) + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs index a5ea022..eeb11a8 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -9,9 +9,12 @@ mod event; mod history; mod import; mod init; +mod login; +mod register; mod search; mod server; mod stats; +mod sync; #[derive(StructOpt)] pub enum AtuinCmd { @@ -38,6 +41,21 @@ pub enum AtuinCmd { #[structopt(about = "interactive history search")] Search { query: Vec }, + + #[structopt(about = "sync with the configured server")] + Sync { + #[structopt(long, short, about = "force re-download everything")] + force: bool, + }, + + #[structopt(about = "login to the configured server")] + Login(login::Cmd), + + #[structopt(about = "register with the configured server")] + Register(register::Cmd), + + #[structopt(about = "print the encryption key for transfer to another machine")] + Key, } pub fn uuid_v4() -> String { @@ -47,13 +65,27 @@ pub fn uuid_v4() -> String { impl AtuinCmd { pub fn run(self, db: &mut impl Database, settings: &Settings) -> Result<()> { match self { - Self::History(history) => history.run(db), + Self::History(history) => history.run(settings, db), Self::Import(import) => import.run(db), Self::Server(server) => server.run(settings), Self::Stats(stats) => stats.run(db, settings), Self::Init => init::init(), Self::Search { query } => search::run(&query, db), + Self::Sync { force } => sync::run(settings, force, db), + Self::Login(l) => l.run(settings), + Self::Register(r) => register::run( + settings, + r.username.as_str(), + r.email.as_str(), + r.password.as_str(), + ), + Self::Key => { + let key = std::fs::read(settings.local.key_path.as_str())?; + println!("{}", base64::encode(key)); + Ok(()) + } + Self::Uuid => { println!("{}", uuid_v4()); Ok(()) diff --git a/src/command/register.rs b/src/command/register.rs new file mode 100644 index 0000000..62bbeae --- /dev/null +++ b/src/command/register.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::prelude::*; + +use eyre::{eyre, Result}; +use structopt::StructOpt; + +use crate::settings::Settings; + +#[derive(StructOpt)] +#[structopt(setting(structopt::clap::AppSettings::DeriveDisplayOrder))] +pub struct Cmd { + #[structopt(long, short)] + pub username: String, + + #[structopt(long, short)] + pub email: String, + + #[structopt(long, short)] + pub password: String, +} + +pub fn run(settings: &Settings, username: &str, email: &str, password: &str) -> Result<()> { + let mut map = HashMap::new(); + map.insert("username", username); + map.insert("email", email); + map.insert("password", password); + + let url = format!("{}/user/{}", settings.local.sync_address, username); + let resp = reqwest::blocking::get(url)?; + + if resp.status().is_success() { + println!("Username is already in use! Please try another."); + return Ok(()); + } + + let url = format!("{}/register", settings.local.sync_address); + let client = reqwest::blocking::Client::new(); + let resp = client.post(url).json(&map).send()?; + + if !resp.status().is_success() { + println!("Failed to register user - please check your details and try again"); + return Err(eyre!("failed to register user")); + } + + let session = resp.json::>()?; + let session = session["session"].clone(); + + let path = settings.local.session_path.as_str(); + let mut file = File::create(path)?; + file.write_all(session.as_bytes())?; + + Ok(()) +} diff --git a/src/command/search.rs b/src/command/search.rs index d51e29e..b9f3987 100644 --- a/src/command/search.rs +++ b/src/command/search.rs @@ -171,7 +171,8 @@ fn select_history(query: &[String], db: &mut impl Database) -> Result { .iter() .enumerate() .map(|(i, m)| { - let mut content = Span::raw(m.command.to_string()); + let mut content = + Span::raw(m.command.to_string().replace("\n", " ").replace("\t", " ")); if let Some(selected) = app.results_state.selected() { if selected == i { diff --git a/src/command/server.rs b/src/command/server.rs index 5156f40..ba2a9a2 100644 --- a/src/command/server.rs +++ b/src/command/server.rs @@ -24,10 +24,10 @@ impl Cmd { match self { Self::Start { host, port } => { let host = host.as_ref().map_or( - settings.remote.host.clone(), + settings.server.host.clone(), std::string::ToString::to_string, ); - let port = port.map_or(settings.remote.port, |p| p); + let port = port.map_or(settings.server.port, |p| p); server::launch(settings, host, port); } diff --git a/src/command/sync.rs b/src/command/sync.rs new file mode 100644 index 0000000..facbe57 --- /dev/null +++ b/src/command/sync.rs @@ -0,0 +1,15 @@ +use eyre::Result; + +use crate::local::database::Database; +use crate::local::sync; +use crate::settings::Settings; + +pub fn run(settings: &Settings, force: bool, db: &mut impl Database) -> Result<()> { + sync::sync(settings, force, db)?; + println!( + "Sync complete! {} items in database, force: {}", + db.history_count()?, + force + ); + Ok(()) +} diff --git a/src/local/api_client.rs b/src/local/api_client.rs new file mode 100644 index 0000000..434c07b --- /dev/null +++ b/src/local/api_client.rs @@ -0,0 +1,94 @@ +use chrono::Utc; +use eyre::Result; +use reqwest::header::AUTHORIZATION; + +use crate::api::{AddHistoryRequest, CountResponse, ListHistoryResponse}; +use crate::local::encryption::{decrypt, load_key}; +use crate::local::history::History; +use crate::settings::Settings; +use crate::utils::hash_str; + +pub struct Client<'a> { + settings: &'a Settings, +} + +impl<'a> Client<'a> { + pub const fn new(settings: &'a Settings) -> Self { + Client { settings } + } + + pub fn count(&self) -> Result { + let url = format!("{}/sync/count", self.settings.local.sync_address); + let client = reqwest::blocking::Client::new(); + + let resp = client + .get(url) + .header( + AUTHORIZATION, + format!("Token {}", self.settings.local.session_token), + ) + .send()?; + + let count = resp.json::()?; + + Ok(count.count) + } + + pub fn get_history( + &self, + sync_ts: chrono::DateTime, + history_ts: chrono::DateTime, + host: Option, + ) -> Result> { + let key = load_key(self.settings)?; + + let host = match host { + None => hash_str(&format!("{}:{}", whoami::hostname(), whoami::username())), + Some(h) => h, + }; + + // this allows for syncing between users on the same machine + let url = format!( + "{}/sync/history?sync_ts={}&history_ts={}&host={}", + self.settings.local.sync_address, + sync_ts.to_rfc3339(), + history_ts.to_rfc3339(), + host, + ); + let client = reqwest::blocking::Client::new(); + + let resp = client + .get(url) + .header( + AUTHORIZATION, + format!("Token {}", self.settings.local.session_token), + ) + .send()?; + + let history = resp.json::()?; + let history = history + .history + .iter() + .map(|h| serde_json::from_str(h).expect("invalid base64")) + .map(|h| decrypt(&h, &key).expect("failed to decrypt history! check your key")) + .collect(); + + Ok(history) + } + + pub fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> { + let client = reqwest::blocking::Client::new(); + + let url = format!("{}/history", self.settings.local.sync_address); + client + .post(url) + .json(history) + .header( + AUTHORIZATION, + format!("Token {}", self.settings.local.session_token), + ) + .send()?; + + Ok(()) + } +} diff --git a/src/local/database.rs b/src/local/database.rs index ad7078e..977f11c 100644 --- a/src/local/database.rs +++ b/src/local/database.rs @@ -1,3 +1,4 @@ +use chrono::prelude::*; use chrono::Utc; use std::path::Path; @@ -21,6 +22,10 @@ pub trait Database { fn update(&self, h: &History) -> Result<()>; fn history_count(&self) -> Result; + fn first(&self) -> Result; + fn last(&self) -> Result; + fn before(&self, timestamp: chrono::DateTime, count: i64) -> Result>; + fn prefix_search(&self, query: &str) -> Result>; } @@ -44,9 +49,7 @@ impl Sqlite { let conn = Connection::open(path)?; - if create { - Self::setup_db(&conn)?; - } + Self::setup_db(&conn)?; Ok(Self { conn }) } @@ -70,6 +73,14 @@ impl Sqlite { [], )?; + conn.execute( + "create table if not exists history_encrypted ( + id text primary key, + data blob not null + )", + [], + )?; + Ok(()) } @@ -87,7 +98,7 @@ impl Sqlite { ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![ h.id, - h.timestamp, + h.timestamp.timestamp_nanos(), h.duration, h.exit, h.command, @@ -146,7 +157,7 @@ impl Database for Sqlite { "update history set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8 where id = ?1", - params![h.id, h.timestamp, h.duration, h.exit, h.command, h.cwd, h.session, h.hostname], + params![h.id, h.timestamp.timestamp_nanos(), h.duration, h.exit, h.command, h.cwd, h.session, h.hostname], )?; Ok(()) @@ -183,6 +194,38 @@ impl Database for Sqlite { Ok(history_iter.filter_map(Result::ok).collect()) } + fn first(&self) -> Result { + let mut stmt = self + .conn + .prepare("SELECT * FROM history order by timestamp asc limit 1")?; + + let history = stmt.query_row(params![], |row| history_from_sqlite_row(None, row))?; + + Ok(history) + } + + fn last(&self) -> Result { + let mut stmt = self + .conn + .prepare("SELECT * FROM history order by timestamp desc limit 1")?; + + let history = stmt.query_row(params![], |row| history_from_sqlite_row(None, row))?; + + Ok(history) + } + + fn before(&self, timestamp: chrono::DateTime, count: i64) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT * FROM history where timestamp <= ? order by timestamp desc limit ?", + )?; + + let history_iter = stmt.query_map(params![timestamp.timestamp_nanos(), count], |row| { + history_from_sqlite_row(None, row) + })?; + + Ok(history_iter.filter_map(Result::ok).collect()) + } + fn query(&self, query: &str, params: impl Params) -> Result> { let mut stmt = self.conn.prepare(query)?; @@ -218,7 +261,7 @@ fn history_from_sqlite_row( Ok(History { id, - timestamp: row.get(1)?, + timestamp: Utc.timestamp_nanos(row.get(1)?), duration: row.get(2)?, exit: row.get(3)?, command: row.get(4)?, diff --git a/src/local/encryption.rs b/src/local/encryption.rs new file mode 100644 index 0000000..3c1699e --- /dev/null +++ b/src/local/encryption.rs @@ -0,0 +1,108 @@ +// The general idea is that we NEVER send cleartext history to the server +// This way the odds of anything private ending up where it should not are +// very low +// The server authenticates via the usual username and password. This has +// nothing to do with the encryption, and is purely authentication! The client +// generates its own secret key, and encrypts all shell history with libsodium's +// secretbox. The data is then sent to the server, where it is stored. All +// clients must share the secret in order to be able to sync, as it is needed +// to decrypt + +use std::fs::File; +use std::io::prelude::*; +use std::path::PathBuf; + +use eyre::{eyre, Result}; +use sodiumoxide::crypto::secretbox; + +use crate::local::history::History; +use crate::settings::Settings; + +#[derive(Debug, Serialize, Deserialize)] +pub struct EncryptedHistory { + pub ciphertext: Vec, + pub nonce: secretbox::Nonce, +} + +// Loads the secret key, will create + save if it doesn't exist +pub fn load_key(settings: &Settings) -> Result { + let path = settings.local.key_path.as_str(); + + if PathBuf::from(path).exists() { + let bytes = std::fs::read(path)?; + let key: secretbox::Key = rmp_serde::from_read_ref(&bytes)?; + Ok(key) + } else { + let key = secretbox::gen_key(); + let buf = rmp_serde::to_vec(&key)?; + + let mut file = File::create(path)?; + file.write_all(&buf)?; + + Ok(key) + } +} + +pub fn encrypt(history: &History, key: &secretbox::Key) -> Result { + // serialize with msgpack + let buf = rmp_serde::to_vec(history)?; + + let nonce = secretbox::gen_nonce(); + + let ciphertext = secretbox::seal(&buf, &nonce, key); + + Ok(EncryptedHistory { ciphertext, nonce }) +} + +pub fn decrypt(encrypted_history: &EncryptedHistory, key: &secretbox::Key) -> Result { + let plaintext = secretbox::open(&encrypted_history.ciphertext, &encrypted_history.nonce, key) + .map_err(|_| eyre!("failed to open secretbox - invalid key?"))?; + + let history = rmp_serde::from_read_ref(&plaintext)?; + + Ok(history) +} + +#[cfg(test)] +mod test { + use sodiumoxide::crypto::secretbox; + + use crate::local::history::History; + + use super::{decrypt, encrypt}; + + #[test] + fn test_encrypt_decrypt() { + let key1 = secretbox::gen_key(); + let key2 = secretbox::gen_key(); + + let history = History::new( + chrono::Utc::now(), + "ls".to_string(), + "/home/ellie".to_string(), + 0, + 1, + Some("beep boop".to_string()), + Some("booop".to_string()), + ); + + let e1 = encrypt(&history, &key1).unwrap(); + let e2 = encrypt(&history, &key2).unwrap(); + + assert_ne!(e1.ciphertext, e2.ciphertext); + assert_ne!(e1.nonce, e2.nonce); + + // test decryption works + // this should pass + match decrypt(&e1, &key1) { + Err(e) => assert!(false, "failed to decrypt, got {}", e), + Ok(h) => assert_eq!(h, history), + }; + + // this should err + match decrypt(&e2, &key1) { + Ok(_) => assert!(false, "expected an error decrypting with invalid key"), + Err(_) => {} + }; + } +} diff --git a/src/local/history.rs b/src/local/history.rs index 0ca112b..1712f8b 100644 --- a/src/local/history.rs +++ b/src/local/history.rs @@ -1,12 +1,15 @@ use std::env; use std::hash::{Hash, Hasher}; +use chrono::Utc; + use crate::command::uuid_v4; -#[derive(Debug, Clone)] +// Any new fields MUST be Optional<>! +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct History { pub id: String, - pub timestamp: i64, + pub timestamp: chrono::DateTime, pub duration: i64, pub exit: i64, pub command: String, @@ -17,7 +20,7 @@ pub struct History { impl History { pub fn new( - timestamp: i64, + timestamp: chrono::DateTime, command: String, cwd: String, exit: i64, @@ -29,7 +32,7 @@ impl History { .or_else(|| env::var("ATUIN_SESSION").ok()) .unwrap_or_else(uuid_v4); let hostname = - hostname.unwrap_or_else(|| hostname::get().unwrap().to_str().unwrap().to_string()); + hostname.unwrap_or_else(|| format!("{}:{}", whoami::hostname(), whoami::username())); Self { id: uuid_v4(), diff --git a/src/local/import.rs b/src/local/import.rs index 9bf79c7..d0f679c 100644 --- a/src/local/import.rs +++ b/src/local/import.rs @@ -4,7 +4,9 @@ use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::{fs::File, path::Path}; -use eyre::{Result, WrapErr}; +use chrono::prelude::*; +use chrono::Utc; +use eyre::{eyre, Result}; use super::history::History; @@ -13,6 +15,7 @@ pub struct Zsh { file: BufReader, pub loc: u64, + pub counter: i64, } // this could probably be sped up @@ -32,19 +35,23 @@ impl Zsh { Ok(Self { file: buf, loc: loc as u64, + counter: 0, }) } } -fn parse_extended(line: &str) -> History { +fn parse_extended(line: &str, counter: i64) -> History { let line = line.replacen(": ", "", 2); let (time, duration) = line.split_once(':').unwrap(); let (duration, command) = duration.split_once(';').unwrap(); - let time = time.parse::().map_or_else( - |_| chrono::Utc::now().timestamp_nanos(), - |t| t * 1_000_000_000, - ); + let time = time + .parse::() + .unwrap_or_else(|_| chrono::Utc::now().timestamp()); + + let offset = chrono::Duration::milliseconds(counter); + let time = Utc.timestamp(time, 0); + let time = time + offset; let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000); @@ -60,6 +67,18 @@ fn parse_extended(line: &str) -> History { ) } +impl Zsh { + fn read_line(&mut self) -> Option> { + let mut line = String::new(); + + match self.file.read_line(&mut line) { + Ok(0) => None, + Ok(_) => Some(Ok(line)), + Err(e) => Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 + } + } +} + impl Iterator for Zsh { type Item = Result; @@ -68,54 +87,89 @@ impl Iterator for Zsh { // These lines begin with : // So, if the line begins with :, parse it. Otherwise it's just // the command - let mut line = String::new(); + let line = self.read_line()?; - match self.file.read_line(&mut line) { - Ok(0) => None, - Ok(_) => { - let extended = line.starts_with(':'); + if let Err(e) = line { + return Some(Err(e)); // :( + } - if extended { - Some(Ok(parse_extended(line.as_str()))) - } else { - Some(Ok(History::new( - chrono::Utc::now().timestamp_nanos(), // what else? :/ - line.trim_end().to_string(), - String::from("unknown"), - -1, - -1, - None, - None, - ))) - } + let mut line = line.unwrap(); + + while line.ends_with("\\\n") { + let next_line = self.read_line()?; + + if next_line.is_err() { + // There's a chance that the last line of a command has invalid + // characters, the only safe thing to do is break :/ + // usually just invalid utf8 or smth + // however, we really need to avoid missing history, so it's + // better to have some items that should have been part of + // something else, than to miss things. So break. + break; } - Err(e) => Some(Err(e).wrap_err("failed to parse line")), + + line.push_str(next_line.unwrap().as_str()); + } + + // We have to handle the case where a line has escaped newlines. + // Keep reading until we have a non-escaped newline + + let extended = line.starts_with(':'); + + if extended { + self.counter += 1; + Some(Ok(parse_extended(line.as_str(), self.counter))) + } else { + let time = chrono::Utc::now(); + let offset = chrono::Duration::seconds(self.counter); + let time = time - offset; + + self.counter += 1; + + Some(Ok(History::new( + time, + line.trim_end().to_string(), + String::from("unknown"), + -1, + -1, + None, + None, + ))) } } } #[cfg(test)] mod test { + use chrono::prelude::*; + use chrono::Utc; + use super::parse_extended; #[test] fn test_parse_extended_simple() { - let parsed = parse_extended(": 1613322469:0;cargo install atuin"); + let parsed = parse_extended(": 1613322469:0;cargo install atuin", 0); assert_eq!(parsed.command, "cargo install atuin"); assert_eq!(parsed.duration, 0); - assert_eq!(parsed.timestamp, 1_613_322_469_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - let parsed = parse_extended(": 1613322469:10;cargo install atuin;cargo update"); + let parsed = parse_extended(": 1613322469:10;cargo install atuin;cargo update", 0); assert_eq!(parsed.command, "cargo install atuin;cargo update"); assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, 1_613_322_469_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); - let parsed = parse_extended(": 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷"); + let parsed = parse_extended(": 1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0); assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷"); assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, 1_613_322_469_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + + let parsed = parse_extended(": 1613322469:10;cargo install \\n atuin\n", 0); + + assert_eq!(parsed.command, "cargo install \\n atuin"); + assert_eq!(parsed.duration, 10_000_000_000); + assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); } } diff --git a/src/local/mod.rs b/src/local/mod.rs index a11ee21..9fe3129 100644 --- a/src/local/mod.rs +++ b/src/local/mod.rs @@ -1,3 +1,6 @@ +pub mod api_client; pub mod database; +pub mod encryption; pub mod history; pub mod import; +pub mod sync; diff --git a/src/local/sync.rs b/src/local/sync.rs new file mode 100644 index 0000000..c22d2f2 --- /dev/null +++ b/src/local/sync.rs @@ -0,0 +1,135 @@ +use std::convert::TryInto; + +use chrono::prelude::*; +use eyre::Result; + +use crate::local::api_client; +use crate::local::database::Database; +use crate::local::encryption::{encrypt, load_key}; +use crate::settings::{Local, Settings, HISTORY_PAGE_SIZE}; +use crate::{api::AddHistoryRequest, utils::hash_str}; + +// Currently sync is kinda naive, and basically just pages backwards through +// history. This means newly added stuff shows up properly! We also just use +// the total count in each database to indicate whether a sync is needed. +// I think this could be massively improved! If we had a way of easily +// indicating count per time period (hour, day, week, year, etc) then we can +// easily pinpoint where we are missing data and what needs downloading. Start +// with year, then find the week, then the day, then the hour, then download it +// all! The current naive approach will do for now. + +// Check if remote has things we don't, and if so, download them. +// Returns (num downloaded, total local) +fn sync_download( + force: bool, + client: &api_client::Client, + db: &mut impl Database, +) -> Result<(i64, i64)> { + let remote_count = client.count()?; + + let initial_local = db.history_count()?; + let mut local_count = initial_local; + + let mut last_sync = if force { + Utc.timestamp_millis(0) + } else { + Local::last_sync()? + }; + + let mut last_timestamp = Utc.timestamp_millis(0); + + let host = if force { Some(String::from("")) } else { None }; + + while remote_count > local_count { + let page = client.get_history(last_sync, last_timestamp, host.clone())?; + + if page.len() < HISTORY_PAGE_SIZE.try_into().unwrap() { + break; + } + + db.save_bulk(&page)?; + + local_count = db.history_count()?; + + let page_last = page + .last() + .expect("could not get last element of page") + .timestamp; + + // in the case of a small sync frequency, it's possible for history to + // be "lost" between syncs. In this case we need to rewind the sync + // timestamps + if page_last == last_timestamp { + last_timestamp = Utc.timestamp_millis(0); + last_sync = last_sync - chrono::Duration::hours(1); + } else { + last_timestamp = page_last; + } + } + + Ok((local_count - initial_local, local_count)) +} + +// Check if we have things remote doesn't, and if so, upload them +fn sync_upload( + settings: &Settings, + _force: bool, + client: &api_client::Client, + db: &mut impl Database, +) -> Result<()> { + let initial_remote_count = client.count()?; + let mut remote_count = initial_remote_count; + + let local_count = db.history_count()?; + + let key = load_key(settings)?; // encryption key + + // first just try the most recent set + + let mut cursor = Utc::now(); + + while local_count > remote_count { + let last = db.before(cursor, HISTORY_PAGE_SIZE)?; + let mut buffer = Vec::::new(); + + if last.is_empty() { + break; + } + + for i in last { + let data = encrypt(&i, &key)?; + let data = serde_json::to_string(&data)?; + + let add_hist = AddHistoryRequest { + id: i.id, + timestamp: i.timestamp, + data, + hostname: hash_str(i.hostname.as_str()), + }; + + buffer.push(add_hist); + } + + // anything left over outside of the 100 block size + client.post_history(&buffer)?; + cursor = buffer.last().unwrap().timestamp; + + remote_count = client.count()?; + } + + Ok(()) +} + +pub fn sync(settings: &Settings, force: bool, db: &mut impl Database) -> Result<()> { + let client = api_client::Client::new(settings); + + sync_upload(settings, force, &client, db)?; + + let download = sync_download(force, &client, db)?; + + debug!("sync downloaded {}", download.0); + + Local::save_sync_time()?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index bac7536..ae45980 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use eyre::{eyre, Result}; -use structopt::StructOpt; +use structopt::{clap::AppSettings, StructOpt}; #[macro_use] extern crate log; @@ -30,18 +30,21 @@ use command::AtuinCmd; use local::database::Sqlite; use settings::Settings; +mod api; mod command; mod local; mod remote; mod settings; +mod utils; pub mod schema; #[derive(StructOpt)] #[structopt( author = "Ellie Huxtable ", - version = "0.4.0", - about = "Magical shell history" + version = "0.5.0", + about = "Magical shell history", + global_settings(&[AppSettings::ColoredHelp, AppSettings::DeriveDisplayOrder]) )] struct Atuin { #[structopt(long, parse(from_os_str), help = "db file path")] @@ -52,9 +55,7 @@ struct Atuin { } impl Atuin { - fn run(self) -> Result<()> { - let settings = Settings::new()?; - + fn run(self, settings: &Settings) -> Result<()> { let db_path = if let Some(db_path) = self.db { let path = db_path .to_str() @@ -67,11 +68,13 @@ impl Atuin { let mut db = Sqlite::new(db_path)?; - self.atuin.run(&mut db, &settings) + self.atuin.run(&mut db, settings) } } fn main() -> Result<()> { + let settings = Settings::new()?; + fern::Dispatch::new() .format(|out, message, record| { out.finish(format_args!( @@ -85,5 +88,5 @@ fn main() -> Result<()> { .chain(std::io::stdout()) .apply()?; - Atuin::from_args().run() + Atuin::from_args().run(&settings) } diff --git a/src/remote/auth.rs b/src/remote/auth.rs index 8f9e9b4..cf61b07 100644 --- a/src/remote/auth.rs +++ b/src/remote/auth.rs @@ -1,6 +1,8 @@ use self::diesel::prelude::*; +use eyre::Result; use rocket::http::Status; use rocket::request::{self, FromRequest, Outcome, Request}; +use rocket::State; use rocket_contrib::databases::diesel; use sodiumoxide::crypto::pwhash::argon2id13; @@ -9,7 +11,11 @@ use uuid::Uuid; use super::models::{NewSession, NewUser, Session, User}; use super::views::ApiResponse; + +use crate::api::{LoginRequest, RegisterRequest}; use crate::schema::{sessions, users}; +use crate::settings::Settings; +use crate::utils::hash_secret; use super::database::AtuinDbConn; @@ -19,20 +25,6 @@ pub enum KeyError { Invalid, } -pub fn hash_str(secret: &str) -> String { - sodiumoxide::init().unwrap(); - let hash = argon2id13::pwhash( - secret.as_bytes(), - argon2id13::OPSLIMIT_INTERACTIVE, - argon2id13::MEMLIMIT_INTERACTIVE, - ) - .unwrap(); - let texthash = std::str::from_utf8(&hash.0).unwrap().to_string(); - - // postgres hates null chars. don't do that to postgres - texthash.trim_end_matches('\u{0}').to_string() -} - pub fn verify_str(secret: &str, verify: &str) -> bool { sodiumoxide::init().unwrap(); @@ -95,19 +87,54 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { } } -#[derive(Deserialize)] -pub struct Register { - email: String, - password: String, +#[get("/user/")] +#[allow(clippy::clippy::needless_pass_by_value)] +pub fn get_user(user: String, conn: AtuinDbConn) -> ApiResponse { + use crate::schema::users::dsl::{username, users}; + + let user: Result = users + .select(username) + .filter(username.eq(user)) + .first(&*conn); + + if user.is_err() { + return ApiResponse { + json: json!({ + "message": "could not find user", + }), + status: Status::NotFound, + }; + } + + let user = user.unwrap(); + + ApiResponse { + json: json!({ "username": user.as_str() }), + status: Status::Ok, + } } #[post("/register", data = "")] #[allow(clippy::clippy::needless_pass_by_value)] -pub fn register(conn: AtuinDbConn, register: Json) -> ApiResponse { - let hashed = hash_str(register.password.as_str()); +pub fn register( + conn: AtuinDbConn, + register: Json, + settings: State, +) -> ApiResponse { + if !settings.server.open_registration { + return ApiResponse { + status: Status::BadRequest, + json: json!({ + "message": "registrations are not open" + }), + }; + } + + let hashed = hash_secret(register.password.as_str()); let new_user = NewUser { email: register.email.as_str(), + username: register.username.as_str(), password: hashed.as_str(), }; @@ -119,8 +146,7 @@ pub fn register(conn: AtuinDbConn, register: Json) -> ApiResponse { return ApiResponse { status: Status::BadRequest, json: json!({ - "status": "error", - "message": "failed to create user - is the email already in use?", + "message": "failed to create user - username or email in use?", }), }; } @@ -139,32 +165,26 @@ pub fn register(conn: AtuinDbConn, register: Json) -> ApiResponse { { Ok(_) => ApiResponse { status: Status::Ok, - json: json!({"status": "ok", "message": "user created!", "session": token}), + json: json!({"message": "user created!", "session": token}), }, Err(_) => ApiResponse { status: Status::BadRequest, - json: json!({"status": "error", "message": "failed to create user"}), + json: json!({ "message": "failed to create user"}), }, } } -#[derive(Deserialize)] -pub struct Login { - email: String, - password: String, -} - #[post("/login", data = "")] #[allow(clippy::clippy::needless_pass_by_value)] -pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { +pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { let user = users::table - .filter(users::email.eq(login.email.as_str())) + .filter(users::username.eq(login.username.as_str())) .first(&*conn); if user.is_err() { return ApiResponse { status: Status::NotFound, - json: json!({"status": "error", "message": "user not found"}), + json: json!({"message": "user not found"}), }; } @@ -178,7 +198,7 @@ pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { if session.is_err() { return ApiResponse { status: Status::InternalServerError, - json: json!({"status": "error", "message": "something went wrong"}), + json: json!({"message": "something went wrong"}), }; } @@ -187,7 +207,7 @@ pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { if !verified { return ApiResponse { status: Status::NotFound, - json: json!({"status": "error", "message": "user not found"}), + json: json!({"message": "user not found"}), }; } @@ -195,6 +215,6 @@ pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { ApiResponse { status: Status::Ok, - json: json!({"status": "ok", "token": session.token}), + json: json!({"session": session.token}), } } diff --git a/src/remote/database.rs b/src/remote/database.rs index fabd07d..ddcffda 100644 --- a/src/remote/database.rs +++ b/src/remote/database.rs @@ -8,7 +8,7 @@ pub struct AtuinDbConn(diesel::PgConnection); // TODO: connection pooling pub fn establish_connection(settings: &Settings) -> PgConnection { - let database_url = &settings.remote.db_uri; + let database_url = &settings.server.db_uri; PgConnection::establish(database_url) .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } diff --git a/src/remote/models.rs b/src/remote/models.rs index 058b2f0..7f6f776 100644 --- a/src/remote/models.rs +++ b/src/remote/models.rs @@ -1,23 +1,26 @@ -use chrono::naive::NaiveDateTime; +use chrono::prelude::*; use crate::schema::{history, sessions, users}; -#[derive(Identifiable, Queryable, Associations)] +#[derive(Deserialize, Serialize, Identifiable, Queryable, Associations)] #[table_name = "history"] #[belongs_to(User)] pub struct History { pub id: i64, - pub client_id: String, + pub client_id: String, // a client generated ID pub user_id: i64, - pub mac: String, + pub hostname: String, pub timestamp: NaiveDateTime, pub data: String, + + pub created_at: NaiveDateTime, } #[derive(Identifiable, Queryable, Associations)] pub struct User { pub id: i64, + pub username: String, pub email: String, pub password: String, } @@ -35,8 +38,8 @@ pub struct Session { pub struct NewHistory<'a> { pub client_id: &'a str, pub user_id: i64, - pub mac: &'a str, - pub timestamp: NaiveDateTime, + pub hostname: String, + pub timestamp: chrono::NaiveDateTime, pub data: &'a str, } @@ -44,6 +47,7 @@ pub struct NewHistory<'a> { #[derive(Insertable)] #[table_name = "users"] pub struct NewUser<'a> { + pub username: &'a str, pub email: &'a str, pub password: &'a str, } diff --git a/src/remote/server.rs b/src/remote/server.rs index cd2ca7b..de58397 100644 --- a/src/remote/server.rs +++ b/src/remote/server.rs @@ -17,13 +17,15 @@ use super::auth::*; embed_migrations!("migrations"); pub fn launch(settings: &Settings, host: String, port: u16) { + let settings: Settings = settings.clone(); // clone so rocket can manage it + let mut database_config = HashMap::new(); let mut databases = HashMap::new(); - database_config.insert("url", Value::from(settings.remote.db_uri.clone())); + database_config.insert("url", Value::from(settings.server.db_uri.clone())); databases.insert("atuin", Value::from(database_config)); - let connection = establish_connection(settings); + let connection = establish_connection(&settings); embedded_migrations::run(&connection).expect("failed to run migrations"); let config = Config::build(Environment::Production) @@ -36,8 +38,20 @@ pub fn launch(settings: &Settings, host: String, port: u16) { let app = rocket::custom(config); - app.mount("/", routes![index, register, add_history, login]) - .attach(AtuinDbConn::fairing()) - .register(catchers![internal_error, bad_request]) - .launch(); + app.mount( + "/", + routes![ + index, + register, + add_history, + login, + get_user, + sync_count, + sync_list + ], + ) + .manage(settings) + .attach(AtuinDbConn::fairing()) + .register(catchers![internal_error, bad_request]) + .launch(); } diff --git a/src/remote/views.rs b/src/remote/views.rs index 2af3f36..08dff13 100644 --- a/src/remote/views.rs +++ b/src/remote/views.rs @@ -1,14 +1,22 @@ -use self::diesel::prelude::*; +use chrono::Utc; +use rocket::http::uri::Uri; +use rocket::http::RawStr; use rocket::http::{ContentType, Status}; +use rocket::request::FromFormValue; use rocket::request::Request; use rocket::response; use rocket::response::{Responder, Response}; use rocket_contrib::databases::diesel; use rocket_contrib::json::{Json, JsonValue}; -use super::database::AtuinDbConn; -use super::models::{NewHistory, User}; +use self::diesel::prelude::*; + +use crate::api::AddHistoryRequest; use crate::schema::history; +use crate::settings::HISTORY_PAGE_SIZE; + +use super::database::AtuinDbConn; +use super::models::{History, NewHistory, User}; #[derive(Debug)] pub struct ApiResponse { @@ -46,40 +54,36 @@ pub fn bad_request(_req: &Request) -> ApiResponse { } } -#[derive(Deserialize)] -pub struct AddHistory { - id: String, - timestamp: i64, - data: String, - mac: String, -} - #[post("/history", data = "")] #[allow( clippy::clippy::cast_sign_loss, clippy::cast_possible_truncation, clippy::clippy::needless_pass_by_value )] -pub fn add_history(conn: AtuinDbConn, user: User, add_history: Json) -> ApiResponse { - let secs: i64 = add_history.timestamp / 1_000_000_000; - let nanosecs: u32 = (add_history.timestamp - (secs * 1_000_000_000)) as u32; - let datetime = chrono::NaiveDateTime::from_timestamp(secs, nanosecs); - - let new_history = NewHistory { - client_id: add_history.id.as_str(), - user_id: user.id, - mac: add_history.mac.as_str(), - timestamp: datetime, - data: add_history.data.as_str(), - }; +pub fn add_history( + conn: AtuinDbConn, + user: User, + add_history: Json>, +) -> ApiResponse { + let new_history: Vec = add_history + .iter() + .map(|h| NewHistory { + client_id: h.id.as_str(), + hostname: h.hostname.to_string(), + user_id: user.id, + timestamp: h.timestamp.naive_utc(), + data: h.data.as_str(), + }) + .collect(); match diesel::insert_into(history::table) .values(&new_history) + .on_conflict_do_nothing() .execute(&*conn) { Ok(_) => ApiResponse { status: Status::Ok, - json: json!({"status": "ok", "message": "history added", "id": new_history.client_id}), + json: json!({"status": "ok", "message": "history added"}), }, Err(_) => ApiResponse { status: Status::BadRequest, @@ -87,3 +91,95 @@ pub fn add_history(conn: AtuinDbConn, user: User, add_history: Json) }, } } + +#[get("/sync/count")] +#[allow(clippy::wildcard_imports, clippy::needless_pass_by_value)] +pub fn sync_count(conn: AtuinDbConn, user: User) -> ApiResponse { + use crate::schema::history::dsl::*; + + // we need to return the number of history items we have for this user + // in the future I'd like to use something like a merkel tree to calculate + // which day specifically needs syncing + let count = history + .filter(user_id.eq(user.id)) + .count() + .first::(&*conn); + + if count.is_err() { + error!("failed to count: {}", count.err().unwrap()); + + return ApiResponse { + json: json!({"message": "internal server error"}), + status: Status::InternalServerError, + }; + } + + ApiResponse { + status: Status::Ok, + json: json!({"count": count.ok()}), + } +} + +pub struct UtcDateTime(chrono::DateTime); + +impl<'v> FromFormValue<'v> for UtcDateTime { + type Error = &'v RawStr; + + fn from_form_value(form_value: &'v RawStr) -> Result { + let time = Uri::percent_decode(form_value.as_bytes()).map_err(|_| form_value)?; + let time = time.to_string(); + + match chrono::DateTime::parse_from_rfc3339(time.as_str()) { + Ok(t) => Ok(UtcDateTime(t.with_timezone(&Utc))), + Err(e) => { + error!("failed to parse time {}, got: {}", time, e); + Err(form_value) + } + } + } +} + +// Request a list of all history items added to the DB after a given timestamp. +// Provide the current hostname, so that we don't send the client data that +// originated from them +#[get("/sync/history?&&")] +#[allow(clippy::wildcard_imports, clippy::needless_pass_by_value)] +pub fn sync_list( + conn: AtuinDbConn, + user: User, + sync_ts: UtcDateTime, + history_ts: UtcDateTime, + host: String, +) -> ApiResponse { + use crate::schema::history::dsl::*; + + // we need to return the number of history items we have for this user + // in the future I'd like to use something like a merkel tree to calculate + // which day specifically needs syncing + // TODO: Allow for configuring the page size, both from params, and setting + // the max in config. 100 is fine for now. + let h = history + .filter(user_id.eq(user.id)) + .filter(hostname.ne(host)) + .filter(created_at.ge(sync_ts.0.naive_utc())) + .filter(timestamp.ge(history_ts.0.naive_utc())) + .order(timestamp.asc()) + .limit(HISTORY_PAGE_SIZE) + .load::(&*conn); + + if let Err(e) = h { + error!("failed to load history: {}", e); + + return ApiResponse { + json: json!({"message": "internal server error"}), + status: Status::InternalServerError, + }; + } + + let user_data: Vec = h.unwrap().iter().map(|i| i.data.to_string()).collect(); + + ApiResponse { + status: Status::Ok, + json: json!({ "history": user_data }), + } +} diff --git a/src/schema.rs b/src/schema.rs index efa9ddc..84bf5ba 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -3,9 +3,10 @@ table! { id -> Int8, client_id -> Text, user_id -> Int8, - mac -> Varchar, + hostname -> Text, timestamp -> Timestamp, data -> Varchar, + created_at -> Timestamp, } } @@ -20,6 +21,7 @@ table! { table! { users (id) { id -> Int8, + username -> Varchar, email -> Varchar, password -> Varchar, } diff --git a/src/settings.rs b/src/settings.rs index 0e554be..dcf69a7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,31 +1,90 @@ -use std::path::PathBuf; +use std::fs::{create_dir_all, File}; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; -use config::{Config, File}; +use chrono::prelude::*; +use chrono::Utc; +use config::{Config, File as ConfigFile}; use directories::ProjectDirs; use eyre::{eyre, Result}; -use std::fs; +use parse_duration::parse; -#[derive(Debug, Deserialize)] +pub const HISTORY_PAGE_SIZE: i64 = 100; + +#[derive(Clone, Debug, Deserialize)] pub struct Local { pub dialect: String, - pub sync: bool, + pub auto_sync: bool, pub sync_address: String, pub sync_frequency: String, pub db_path: String, + pub key_path: String, + pub session_path: String, + + // This is automatically loaded when settings is created. Do not set in + // config! Keep secrets and settings apart. + pub session_token: String, } -#[derive(Debug, Deserialize)] -pub struct Remote { +impl Local { + pub fn save_sync_time() -> Result<()> { + let sync_time_path = ProjectDirs::from("com", "elliehuxtable", "atuin") + .ok_or_else(|| eyre!("could not determine key file location"))?; + let sync_time_path = sync_time_path.data_dir().join("last_sync_time"); + + std::fs::write(sync_time_path, Utc::now().to_rfc3339())?; + + Ok(()) + } + + pub fn last_sync() -> Result> { + let sync_time_path = ProjectDirs::from("com", "elliehuxtable", "atuin"); + + if sync_time_path.is_none() { + debug!("failed to load projectdirs, not syncing"); + return Err(eyre!("could not load project dirs")); + } + + let sync_time_path = sync_time_path.unwrap(); + let sync_time_path = sync_time_path.data_dir().join("last_sync_time"); + + if !sync_time_path.exists() { + return Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)); + } + + let time = std::fs::read_to_string(sync_time_path)?; + let time = chrono::DateTime::parse_from_rfc3339(time.as_str())?; + + Ok(time.with_timezone(&Utc)) + } + + pub fn should_sync(&self) -> Result { + if !self.auto_sync { + return Ok(false); + } + + match parse(self.sync_frequency.as_str()) { + Ok(d) => { + let d = chrono::Duration::from_std(d).unwrap(); + Ok(Utc::now() - Local::last_sync()? >= d) + } + Err(e) => Err(eyre!("failed to check sync: {}", e)), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Server { pub host: String, pub port: u16, pub db_uri: String, pub open_registration: bool, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct Settings { pub local: Local, - pub remote: Remote, + pub server: Server, } impl Settings { @@ -33,7 +92,7 @@ impl Settings { let config_dir = ProjectDirs::from("com", "elliehuxtable", "atuin").unwrap(); let config_dir = config_dir.config_dir(); - fs::create_dir_all(config_dir)?; + create_dir_all(config_dir)?; let mut config_file = PathBuf::new(); config_file.push(config_dir); @@ -45,31 +104,61 @@ impl Settings { let mut s = Config::new(); let db_path = ProjectDirs::from("com", "elliehuxtable", "atuin") - .ok_or_else(|| { - eyre!("could not determine db file location\nspecify one using the --db flag") - })? + .ok_or_else(|| eyre!("could not determine db file location"))? .data_dir() .join("history.db"); - s.set_default("local.db_path", db_path.to_str())?; - s.set_default("local.dialect", "us")?; - s.set_default("local.sync", false)?; - s.set_default("local.sync_frequency", "5m")?; - s.set_default("local.sync_address", "https://atuin.ellie.wtf")?; + let key_path = ProjectDirs::from("com", "elliehuxtable", "atuin") + .ok_or_else(|| eyre!("could not determine key file location"))? + .data_dir() + .join("key"); - s.set_default("remote.host", "127.0.0.1")?; - s.set_default("remote.port", 8888)?; - s.set_default("remote.open_registration", false)?; - s.set_default("remote.db_uri", "please set a postgres url")?; + let session_path = ProjectDirs::from("com", "elliehuxtable", "atuin") + .ok_or_else(|| eyre!("could not determine session file location"))? + .data_dir() + .join("session"); + + s.set_default("local.db_path", db_path.to_str())?; + s.set_default("local.key_path", key_path.to_str())?; + s.set_default("local.session_path", session_path.to_str())?; + s.set_default("local.dialect", "us")?; + s.set_default("local.auto_sync", true)?; + s.set_default("local.sync_frequency", "5m")?; + s.set_default("local.sync_address", "https://api.atuin.sh")?; + + s.set_default("server.host", "127.0.0.1")?; + s.set_default("server.port", 8888)?; + s.set_default("server.open_registration", false)?; + s.set_default("server.db_uri", "please set a postgres url")?; if config_file.exists() { - s.merge(File::with_name(config_file.to_str().unwrap()))?; + s.merge(ConfigFile::with_name(config_file.to_str().unwrap()))?; + } else { + let example_config = include_bytes!("../config.toml"); + let mut file = File::create(config_file)?; + file.write_all(example_config)?; } // all paths should be expanded let db_path = s.get_str("local.db_path")?; let db_path = shellexpand::full(db_path.as_str())?; - s.set("local.db.path", db_path.to_string())?; + s.set("local.db_path", db_path.to_string())?; + + let key_path = s.get_str("local.key_path")?; + let key_path = shellexpand::full(key_path.as_str())?; + s.set("local.key_path", key_path.to_string())?; + + let session_path = s.get_str("local.session_path")?; + let session_path = shellexpand::full(session_path.as_str())?; + s.set("local.session_path", session_path.to_string())?; + + // Finally, set the auth token + if Path::new(session_path.to_string().as_str()).exists() { + let token = std::fs::read_to_string(session_path.to_string())?; + s.set("local.session_token", token)?; + } else { + s.set("local.session_token", "not logged in")?; + } s.try_into() .map_err(|e| eyre!("failed to deserialize: {}", e)) diff --git a/src/shell/atuin.zsh b/src/shell/atuin.zsh index 8407efd..d2abf3c 100644 --- a/src/shell/atuin.zsh +++ b/src/shell/atuin.zsh @@ -1,4 +1,6 @@ # Source this in your ~/.zshrc +autoload -U add-zsh-hook + export ATUIN_SESSION=$(atuin uuid) export ATUIN_HISTORY="atuin history list" export ATUIN_BINDKEYS="true" @@ -20,24 +22,12 @@ _atuin_search(){ emulate -L zsh zle -I + # Switch to cursor mode, then back to application + echoti rmkx # swap stderr and stdout, so that the tui stuff works # TODO: not this output=$(atuin search $BUFFER 3>&1 1>&2 2>&3) - - if [[ -n $output ]] ; then - LBUFFER=$output - fi - - zle reset-prompt -} - -_atuin_up_search(){ - emulate -L zsh - zle -I - - # swap stderr and stdout, so that the tui stuff works - # TODO: not this - output=$(atuin search $BUFFER 3>&1 1>&2 2>&3) + echoti smkx if [[ -n $output ]] ; then LBUFFER=$output @@ -50,9 +40,11 @@ add-zsh-hook preexec _atuin_preexec add-zsh-hook precmd _atuin_precmd zle -N _atuin_search_widget _atuin_search -zle -N _atuin_up_search_widget _atuin_up_search if [[ $ATUIN_BINDKEYS == "true" ]]; then bindkey '^r' _atuin_search_widget - bindkey '^[[A' _atuin_up_search_widget + + # depends on terminal mode + bindkey '^[[A' _atuin_search_widget + bindkey '^[OA' _atuin_search_widget fi diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b395b14 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,24 @@ +use crypto::digest::Digest; +use crypto::sha2::Sha256; +use sodiumoxide::crypto::pwhash::argon2id13; + +pub fn hash_secret(secret: &str) -> String { + sodiumoxide::init().unwrap(); + let hash = argon2id13::pwhash( + secret.as_bytes(), + argon2id13::OPSLIMIT_INTERACTIVE, + argon2id13::MEMLIMIT_INTERACTIVE, + ) + .unwrap(); + let texthash = std::str::from_utf8(&hash.0).unwrap().to_string(); + + // postgres hates null chars. don't do that to postgres + texthash.trim_end_matches('\u{0}').to_string() +} + +pub fn hash_str(string: &str) -> String { + let mut hasher = Sha256::new(); + hasher.input_str(string); + + hasher.result_str() +}