aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock954
-rw-r--r--Cargo.toml14
-rw-r--r--config.toml65
-rw-r--r--migrations/2021-03-20-151809_create_history/up.sql6
-rw-r--r--migrations/2021-03-20-171007_create_users/up.sql5
-rw-r--r--src/api.rs36
-rw-r--r--src/command/history.rs30
-rw-r--r--src/command/login.rs48
-rw-r--r--src/command/mod.rs34
-rw-r--r--src/command/register.rs54
-rw-r--r--src/command/search.rs3
-rw-r--r--src/command/server.rs4
-rw-r--r--src/command/sync.rs15
-rw-r--r--src/local/api_client.rs94
-rw-r--r--src/local/database.rs55
-rw-r--r--src/local/encryption.rs108
-rw-r--r--src/local/history.rs11
-rw-r--r--src/local/import.rs116
-rw-r--r--src/local/mod.rs3
-rw-r--r--src/local/sync.rs135
-rw-r--r--src/main.rs19
-rw-r--r--src/remote/auth.rs92
-rw-r--r--src/remote/database.rs2
-rw-r--r--src/remote/models.rs16
-rw-r--r--src/remote/server.rs26
-rw-r--r--src/remote/views.rs144
-rw-r--r--src/schema.rs4
-rw-r--r--src/settings.rs131
-rw-r--r--src/shell/atuin.zsh26
-rw-r--r--src/utils.rs24
30 files changed, 2015 insertions, 259 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b4c2857a..e83d5303 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]]
@@ -224,6 +232,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -236,6 +250,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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,12 +390,28 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -553,6 +590,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -602,6 +648,46 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -621,6 +707,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -637,6 +729,60 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -683,6 +829,25 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -756,6 +932,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -786,6 +1005,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -852,6 +1082,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -867,6 +1103,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -969,12 +1214,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -1017,6 +1256,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1029,13 +1274,26 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1043,7 +1301,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log 0.4.14",
- "mio",
+ "mio 0.6.23",
"slab",
]
@@ -1060,6 +1318,33 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1093,13 +1378,57 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1110,6 +1439,29 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1162,6 +1514,39 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1187,6 +1572,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1221,6 +1617,38 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -1324,15 +1752,50 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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,11 +1805,36 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
@@ -1355,12 +1843,39 @@ dependencies = [
]
[[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]]
@@ -1436,6 +1951,71 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -1544,12 +2124,31 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1577,6 +2176,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1592,6 +2201,29 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1621,17 +2253,29 @@ 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 = "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 = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [
+ "form_urlencoded",
"itoa",
"ryu",
"serde 1.0.125",
@@ -1671,6 +2315,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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,16 +2402,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1832,6 +2500,45 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1850,12 +2557,44 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1947,12 +2686,24 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1997,6 +2748,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2009,6 +2770,94 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2052,6 +2901,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index a016024a..9d337878 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 <e@elm.sh>"]
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 19a454e2..9d5452cb 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 7cb19fc7..4192b04d 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 0eecea7c..46c6a372 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 00000000..90977404
--- /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<Utc>,
+ 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<String>,
+}
diff --git a/src/command/history.rs b/src/command/history.rs
index 05aed4b9..3b4a717c 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 00000000..4f58b77f
--- /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::<HashMap<String, String>>()?;
+ 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 a5ea0228..eeb11a87 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<String> },
+
+ #[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 00000000..62bbeaeb
--- /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::<HashMap<String, String>>()?;
+ 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 d51e29ef..b9f3987c 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<String> {
.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 5156f409..ba2a9a2f 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 00000000..facbe578
--- /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 00000000..434c07ba
--- /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<i64> {
+ 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::<CountResponse>()?;
+
+ Ok(count.count)
+ }
+
+ pub fn get_history(
+ &self,
+ sync_ts: chrono::DateTime<Utc>,
+ history_ts: chrono::DateTime<Utc>,
+ host: Option<String>,
+ ) -> Result<Vec<History>> {
+ 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::<ListHistoryResponse>()?;
+ 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 ad7078e5..977f11cc 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<i64>;
+ fn first(&self) -> Result<History>;
+ fn last(&self) -> Result<History>;
+ fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>>;
+
fn prefix_search(&self, query: &str) -> Result<Vec<History>>;
}
@@ -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<History> {
+ 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<History> {
+ 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<Utc>, count: i64) -> Result<Vec<History>> {
+ 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<Vec<History>> {
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 00000000..3c1699e3
--- /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<u8>,
+ pub nonce: secretbox::Nonce,
+}
+
+// Loads the secret key, will create + save if it doesn't exist
+pub fn load_key(settings: &Settings) -> Result<secretbox::Key> {
+ 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<EncryptedHistory> {
+ // 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<History> {
+ 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 0ca112bd..1712f8b9 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<Utc>,
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<Utc>,
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 9bf79c72..d0f679c9 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<File>,
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::<i64>().map_or_else(
- |_| chrono::Utc::now().timestamp_nanos(),
- |t| t * 1_000_000_000,
- );
+ let time = time
+ .parse::<i64>()
+ .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::<i64>().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<Result<String>> {
+ 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<History>;
@@ -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 a11ee213..9fe31292 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 00000000..c22d2f27
--- /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::<AddHistoryRequest>::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 bac75362..ae459807 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 <e@elm.sh>",
- 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 8f9e9b46..cf61b077 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/<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<String, diesel::result::Error> = 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 = "<register>")]
#[allow(clippy::clippy::needless_pass_by_value)]
-pub fn register(conn: AtuinDbConn, register: Json<Register>) -> ApiResponse {
- let hashed = hash_str(register.password.as_str());
+pub fn register(
+ conn: AtuinDbConn,
+ register: Json<RegisterRequest>,
+ settings: State<Settings>,
+) -> 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<Register>) -> 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<Register>) -> 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 = "<login>")]
#[allow(clippy::clippy::needless_pass_by_value)]
-pub fn login(conn: AtuinDbConn, login: Json<Login>) -> ApiResponse {
+pub fn login(conn: AtuinDbConn, login: Json<LoginRequest>) -> 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<Login>) -> 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<Login>) -> 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<Login>) -> 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 fabd07de..ddcffda0 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 058b2f0b..7f6f7766 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 cd2ca7b8..de58397d 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 2af3f369..08dff13e 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 = "<add_history>")]
#[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<AddHistory>) -> 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<Vec<AddHistoryRequest>>,
+) -> ApiResponse {
+ let new_history: Vec<NewHistory> = 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<AddHistory>)
},
}
}
+
+#[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::<i64>(&*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<Utc>);
+
+impl<'v> FromFormValue<'v> for UtcDateTime {
+ type Error = &'v RawStr;
+
+ fn from_form_value(form_value: &'v RawStr) -> Result<UtcDateTime, &'v RawStr> {
+ 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?<sync_ts>&<history_ts>&<host>")]
+#[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::<History>(&*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<String> = 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 efa9ddcc..84bf5bab 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 0e554bed..dcf69a7c 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<chrono::DateTime<Utc>> {
+ 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<bool> {
+ 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");
+ let key_path = ProjectDirs::from("com", "elliehuxtable", "atuin")
+ .ok_or_else(|| eyre!("could not determine key file location"))?
+ .data_dir()
+ .join("key");
+
+ 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.sync", false)?;
+ s.set_default("local.auto_sync", true)?;
s.set_default("local.sync_frequency", "5m")?;
- s.set_default("local.sync_address", "https://atuin.ellie.wtf")?;
+ s.set_default("local.sync_address", "https://api.atuin.sh")?;
- 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")?;
+ 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 8407efd2..d2abf3c1 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 00000000..b395b148
--- /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()
+}