From c5dae08286c92149766a55bc5696974e89e76dcf Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 24 Dec 2023 23:36:30 +0000 Subject: [PATCH] Add support for cloud sync, specifically GCP This adds generic support for sync to cloud services, with specific spuport for GCP. Adding others -- so long as they support a compare-and-set operation -- should be comparatively straightforward. The cloud support includes cleanup of unnecessary data, and should keep total space usage roughly proportional to the number of tasks. --- Cargo.lock | 733 +++++++++- Cargo.toml | 6 +- doc/man/task-sync.5.in | 47 +- src/Context.cpp | 1 + src/commands/CmdShow.cpp | 1 + src/commands/CmdSync.cpp | 29 +- src/tc/Server.cpp | 38 +- src/tc/Server.h | 13 +- src/tc/rust/Cargo.lock | 1283 ++++++++++++++++- src/tc/rust/Cargo.toml | 3 +- taskchampion/docs/src/SUMMARY.md | 3 + taskchampion/docs/src/encryption.md | 38 + taskchampion/docs/src/http.md | 65 + taskchampion/docs/src/object-store.md | 8 + taskchampion/docs/src/snapshots.md | 11 +- taskchampion/docs/src/sync-model.md | 5 +- taskchampion/docs/src/sync-protocol.md | 171 +-- .../src/bindings_tests/replica.c | 2 +- taskchampion/lib/src/server.rs | 54 +- taskchampion/lib/taskchampion.h | 13 +- taskchampion/taskchampion/Cargo.toml | 17 +- taskchampion/taskchampion/src/errors.rs | 4 + taskchampion/taskchampion/src/lib.rs | 1 + .../taskchampion/src/server/cloud/gcp.rs | 395 +++++ .../taskchampion/src/server/cloud/mod.rs | 16 + .../taskchampion/src/server/cloud/server.rs | 1122 ++++++++++++++ .../taskchampion/src/server/cloud/service.rs | 38 + .../taskchampion/src/server/config.rs | 23 + .../taskchampion/src/server/crypto.rs | 93 +- taskchampion/taskchampion/src/server/mod.rs | 3 + .../taskchampion/src/server/sync/mod.rs | 23 +- 31 files changed, 3901 insertions(+), 358 deletions(-) create mode 100644 taskchampion/docs/src/encryption.md create mode 100644 taskchampion/docs/src/http.md create mode 100644 taskchampion/docs/src/object-store.md create mode 100644 taskchampion/taskchampion/src/server/cloud/gcp.rs create mode 100644 taskchampion/taskchampion/src/server/cloud/mod.rs create mode 100644 taskchampion/taskchampion/src/server/cloud/server.rs create mode 100644 taskchampion/taskchampion/src/server/cloud/service.rs diff --git a/Cargo.lock b/Cargo.lock index 9fbee07ec..72af3b8cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64", + "base64 0.21.0", "bitflags 1.3.2", "brotli", "bytes", @@ -105,7 +105,7 @@ dependencies = [ "futures-util", "mio", "num_cpus", - "socket2", + "socket2 0.4.9", "tokio", "tracing", ] @@ -167,7 +167,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.4.9", "time 0.3.20", "url", ] @@ -184,6 +184,15 @@ dependencies = [ "syn 1.0.104", ] +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -301,18 +310,78 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "async-trait" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bit-set" version = "0.5.2" @@ -384,9 +453,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytestring" @@ -462,6 +531,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -513,6 +588,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -642,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.5.1", ] [[package]] @@ -653,11 +739,10 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] @@ -777,6 +862,85 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "google-cloud-auth" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1087f1fbd2dd3f58c17c7574ddd99cd61cbbbc2c4dc81114b8687209b196cb" +dependencies = [ + "async-trait", + "base64 0.21.0", + "google-cloud-metadata", + "google-cloud-token", + "home", + "jsonwebtoken", + "reqwest", + "serde", + "serde_json", + "thiserror", + "time 0.3.20", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "google-cloud-metadata" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc279bfb50487d7bcd900e8688406475fc750fe474a835b2ab9ade9eb1fc90e2" +dependencies = [ + "reqwest", + "thiserror", + "tokio", +] + +[[package]] +name = "google-cloud-storage" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac04b29849ebdeb9fb008988cc1c4d1f0c9d121b4c7f1ddeb8061df124580e93" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.21.0", + "bytes", + "futures-util", + "google-cloud-auth", + "google-cloud-metadata", + "google-cloud-token", + "hex", + "once_cell", + "percent-encoding", + "pkcs8", + "regex", + "reqwest", + "ring 0.17.3", + "serde", + "serde_json", + "sha2", + "thiserror", + "time 0.3.20", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "google-cloud-token" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c12ba8b21d128a2ce8585955246977fbce4415f680ebf9199b6f9d6d725f" +dependencies = [ + "async-trait", +] + [[package]] name = "h2" version = "0.3.17" @@ -841,6 +1005,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.9" @@ -852,6 +1031,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.7.1" @@ -870,6 +1060,44 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "iana-time-zone" version = "0.1.47" @@ -886,11 +1114,10 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -943,6 +1170,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.7" @@ -988,6 +1221,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.0", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1002,9 +1249,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.146" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libm" @@ -1086,12 +1333,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "memchr" version = "2.6.4" @@ -1104,6 +1345,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.5.1" @@ -1113,16 +1364,36 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", ] [[package]] @@ -1155,11 +1426,20 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.13.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" @@ -1190,17 +1470,35 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1208,6 +1506,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.25" @@ -1360,6 +1668,48 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.22.6", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -1369,12 +1719,26 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rstest" version = "0.17.0" @@ -1415,6 +1779,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1445,19 +1815,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring", + "ring 0.16.20", "rustls-webpki", "sct", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.0", +] + [[package]] name = "rustls-webpki" version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1496,8 +1875,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1560,6 +1939,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1569,6 +1959,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.20", +] + [[package]] name = "slab" version = "0.4.6" @@ -1591,12 +1993,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1652,10 +2080,11 @@ dependencies = [ "byteorder", "chrono", "flate2", + "google-cloud-storage", "log", "pretty_assertions", "proptest", - "ring", + "ring 0.17.3", "rstest", "rusqlite", "serde", @@ -1664,6 +2093,7 @@ dependencies = [ "strum_macros", "tempfile", "thiserror", + "tokio", "ureq", "uuid", ] @@ -1797,19 +2227,42 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.27.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", - "windows-sys 0.45.0", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -1826,6 +2279,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.34" @@ -1835,9 +2294,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "tracing-core" version = "0.1.26" @@ -1847,6 +2318,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.15.0" @@ -1859,11 +2336,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -1873,9 +2359,9 @@ checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] @@ -1886,34 +2372,45 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7830e33f6e25723d41a63f77e434159dad02919f18f55a512b5f16f3b1d77138" dependencies = [ - "base64", + "base64 0.21.0", "flate2", "log", "once_cell", "rustls", "rustls-webpki", "url", - "webpki-roots", + "webpki-roots 0.25.2", ] [[package]] name = "url" -version = "2.2.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1951,6 +2448,15 @@ dependencies = [ "libc", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1982,6 +2488,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.82" @@ -2011,6 +2529,19 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.57" @@ -2021,6 +2552,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "webpki-roots" version = "0.25.2" @@ -2076,6 +2626,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -2106,6 +2665,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2118,6 +2692,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2130,6 +2710,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2142,6 +2728,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2154,6 +2746,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2166,6 +2764,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2178,6 +2782,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2190,6 +2800,21 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "xtask" version = "0.4.1" @@ -2205,6 +2830,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zstd" version = "0.12.3+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 741781869..1a0f1004c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,13 @@ env_logger = "^0.10.0" ffizz-header = "0.5" flate2 = "1" futures = "^0.3.25" +google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] } lazy_static = "1" libc = "0.2.136" log = "^0.4.17" pretty_assertions = "1" proptest = "^1.4.0" -ring = "0.16" +ring = "0.17" rstest = "0.17" rusqlite = { version = "0.29", features = ["bundled"] } serde_json = "^1.0" @@ -40,6 +41,7 @@ serde = { version = "^1.0.147", features = ["derive"] } strum = "0.25" strum_macros = "0.25" tempfile = "3" +tokio = { version = "1", features = ["rt-multi-thread"] } thiserror = "1.0" -ureq = "^2.9.0" +ureq = { version = "^2.8.0", features = ["tls"] } uuid = { version = "^1.6.0", features = ["serde", "v4"] } diff --git a/doc/man/task-sync.5.in b/doc/man/task-sync.5.in index de1f83823..0b54ea531 100644 --- a/doc/man/task-sync.5.in +++ b/doc/man/task-sync.5.in @@ -21,8 +21,35 @@ NOTE: A side-effect of synchronization is that once changes have been synchronized, they cannot be undone. This means that each time synchronization is run, it is no longer possible to undo previous operations. +.SH MANAGING SYNCHRONIZATION + +.SS Adding a Replica + +To add a new replica, configure a new, empty replica identically to +the existing replica, and run `task sync`. + +.SS When to Synchronize + +Taskwarrior can perform a sync operation at every garbage collection (gc) run. +This is the default, and is appropriate for local synchronization. + +For synchronization to a server, a better solution is to run + + $ task sync + +periodically, such as via +.BR cron (8) . + .SH CONFIGURATION +Taskwarrior provides several options for synchronizing your tasks: + + - To a server specifically designed to handle Taskwarrior data. + - To a cloud service such as GCP or AWS. + - To a local, on-disk file. + +.SS Sync Server + To synchronize your tasks to a sync server, you will need the following information from the server administrator: @@ -43,22 +70,20 @@ Configure Taskwarrior with these details: $ task config sync.server.client_id $ task config sync.server.encryption_secret -.SS Adding a Replica - -To add a new replica, configure a new, empty replica identically to -the existing replica, and run `task sync`. +.SS Google Cloud Platform -.SS When to Synchronize +To synchronize your tasks to GCP, use the GCP Console to create a new project, +and within that project a new Cloud Storage bucket. The default settings for +the bucket are adequete. -Taskwarrior can perform a sync operation at every garbage collection (gc) run. -This is the default, and is appropriate for local synchronization. +Authenticate to the project with -For synchronization to a server, a better solution is to run + gcloud config set project $PROJECT_NAME + gcloud auth application-default login - $ task sync +Then configure Taskwarrior with: -periodically, such as via -.BR cron (8) . + $ task config sync.gcp.bucket .SS Local Synchronization diff --git a/src/Context.cpp b/src/Context.cpp index 4d92564fc..aeb607f0f 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -283,6 +283,7 @@ std::string configurationDefaults = "#sync.server.encryption_secret # Encryption secret for sync to a server\n" "#sync.server.origin # Origin of the sync server\n" "#sync.local.server_dir # Directory for local sync\n" + "#sync.gcp.bucket # Bucket for sync to GCP\n" "\n" "# Aliases - alternate names for commands\n" "alias.rm=delete # Alias for the delete command\n" diff --git a/src/commands/CmdShow.cpp b/src/commands/CmdShow.cpp index 94130172c..8c08901ff 100644 --- a/src/commands/CmdShow.cpp +++ b/src/commands/CmdShow.cpp @@ -193,6 +193,7 @@ int CmdShow::execute (std::string& output) " sugar" " summary.all.projects" " sync.local.server_dir" + " sync.gcp.bucket" " sync.server.client_id" " sync.server.encryption_secret" " sync.server.origin" diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index c886f975c..572416540 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -63,17 +63,32 @@ int CmdSync::execute (std::string& output) // If no server is set up, quit. std::string origin = Context::getContext ().config.get ("sync.server.origin"); - std::string client_id = Context::getContext ().config.get ("sync.server.client_id"); - std::string encryption_secret = Context::getContext ().config.get ("sync.server.encryption_secret"); std::string server_dir = Context::getContext ().config.get ("sync.local.server_dir"); + std::string gcp_bucket = Context::getContext ().config.get ("sync.gcp.bucket"); if (server_dir != "") { - server = tc::Server (server_dir); + server = tc::Server::new_local (server_dir); server_ident = server_dir; - } else if (origin != "" && client_id != "" && encryption_secret != "") { - server = tc::Server (origin, client_id, encryption_secret); - server_ident = origin; + } else if (gcp_bucket != "") { + std::string encryption_secret = Context::getContext ().config.get ("sync.gcp.encryption_secret"); + if (encryption_secret == "") { + throw std::string ("sync.gcp.encryption_secret is required"); + } + server = tc::Server::new_gcp (gcp_bucket, encryption_secret); + std::ostringstream os; + os << "GCP bucket " << gcp_bucket; + server_ident = os.str(); + } else if (origin != "") { + std::string client_id = Context::getContext ().config.get ("sync.server.client_id"); + std::string encryption_secret = Context::getContext ().config.get ("sync.server.encryption_secret"); + if (client_id == "" || encryption_secret == "") { + throw std::string ("sync.server.client_id and encryption_secret are required"); + } + server = tc::Server::new_sync (origin, client_id, encryption_secret); + std::ostringstream os; + os << "Sync server at " << origin; + server_ident = os.str(); } else { - throw std::string ("Neither sync.server nor sync.local are configured."); + throw std::string ("No sync.* settings are configured."); } std::stringstream out; diff --git a/src/tc/Server.cpp b/src/tc/Server.cpp index 2f17f4318..0e2920cdb 100644 --- a/src/tc/Server.cpp +++ b/src/tc/Server.cpp @@ -32,7 +32,8 @@ using namespace tc::ffi; //////////////////////////////////////////////////////////////////////////////// -tc::Server::Server (const std::string &server_dir) +tc::Server +tc::Server::new_local (const std::string &server_dir) { TCString tc_server_dir = tc_string_borrow (server_dir.c_str ()); TCString error; @@ -43,18 +44,17 @@ tc::Server::Server (const std::string &server_dir) tc_string_free (&error); throw errmsg; } - inner = unique_tcserver_ptr ( + return Server (unique_tcserver_ptr ( tcserver, - [](TCServer* rep) { tc_server_free (rep); }); + [](TCServer* rep) { tc_server_free (rep); })); } //////////////////////////////////////////////////////////////////////////////// -tc::Server::Server (const std::string &origin, const std::string &client_id, const std::string &encryption_secret) +tc::Server +tc::Server::new_sync (const std::string &origin, const std::string &client_id, const std::string &encryption_secret) { TCString tc_origin = tc_string_borrow (origin.c_str ()); - TCString tc_client_id = tc_string_borrow (client_id.c_str ()); - TCString tc_encryption_secret = tc_string_borrow (encryption_secret.c_str ()); TCUuid tc_client_uuid; @@ -65,16 +65,36 @@ tc::Server::Server (const std::string &origin, const std::string &client_id, con } TCString error; - auto tcserver = tc_server_new_remote (tc_origin, tc_client_uuid, tc_encryption_secret, &error); + auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error); if (!tcserver) { auto errmsg = format ("Could not configure connection to server at {1}: {2}", origin, tc_string_content (&error)); tc_string_free (&error); throw errmsg; } - inner = unique_tcserver_ptr ( + return Server (unique_tcserver_ptr ( tcserver, - [](TCServer* rep) { tc_server_free (rep); }); + [](TCServer* rep) { tc_server_free (rep); })); +} + +//////////////////////////////////////////////////////////////////////////////// +tc::Server +tc::Server::new_gcp (const std::string &bucket, const std::string &encryption_secret) +{ + TCString tc_bucket = tc_string_borrow (bucket.c_str ()); + TCString tc_encryption_secret = tc_string_borrow (encryption_secret.c_str ()); + + TCString error; + auto tcserver = tc_server_new_gcp (tc_bucket, tc_encryption_secret, &error); + if (!tcserver) { + auto errmsg = format ("Could not configure connection to GCP bucket {1}: {2}", + bucket, tc_string_content (&error)); + tc_string_free (&error); + throw errmsg; + } + return Server (unique_tcserver_ptr ( + tcserver, + [](TCServer* rep) { tc_server_free (rep); })); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Server.h b/src/tc/Server.h index 0d2ddf370..07df206f2 100644 --- a/src/tc/Server.h +++ b/src/tc/Server.h @@ -43,7 +43,7 @@ namespace tc { // Server wraps the TCServer type, managing its memory, errors, and so on. // - // Except as noted, method names match the suffix to `tc_replica_..`. + // Except as noted, method names match the suffix to `tc_server_..`. class Server { public: @@ -51,10 +51,13 @@ namespace tc { Server () = default; // Construct a local server (tc_server_new_local). - Server (const std::string& server_dir); + static Server new_local (const std::string& server_dir); - // Construct a remote server (tc_server_new_remote). - Server (const std::string &origin, const std::string &client_id, const std::string &encryption_secret); + // Construct a remote server (tc_server_new_sync). + static Server new_sync (const std::string &origin, const std::string &client_id, const std::string &encryption_secret); + + // Construct a GCP server (tc_server_new_gcp). + static Server new_gcp (const std::string &bucket, const std::string &encryption_secret); // This object "owns" inner, so copy is not allowed. Server (const Server &) = delete; @@ -65,6 +68,8 @@ namespace tc { Server &operator=(Server &&) noexcept; protected: + Server (unique_tcserver_ptr inner) : inner(std::move(inner)) {}; + unique_tcserver_ptr inner; // Replica accesses the inner pointer to call tc_replica_sync diff --git a/src/tc/rust/Cargo.lock b/src/tc/rust/Cargo.lock index 7320a5524..be7cda8a7 100644 --- a/src/tc/rust/Cargo.lock +++ b/src/tc/rust/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -19,6 +28,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -34,24 +52,99 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "async-trait" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -64,11 +157,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cc" -version = "1.0.73" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -87,7 +189,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.43", "wasm-bindgen", "winapi", ] @@ -102,12 +204,37 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -117,6 +244,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cxx" version = "1.0.68" @@ -161,12 +298,58 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -211,30 +394,204 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.4.4", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "matches", "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "google-cloud-auth" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1087f1fbd2dd3f58c17c7574ddd99cd61cbbbc2c4dc81114b8687209b196cb" +dependencies = [ + "async-trait", + "base64 0.21.2", + "google-cloud-metadata", + "google-cloud-token", + "home", + "jsonwebtoken", + "reqwest", + "serde", + "serde_json", + "thiserror", + "time 0.3.31", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "google-cloud-metadata" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc279bfb50487d7bcd900e8688406475fc750fe474a835b2ab9ade9eb1fc90e2" +dependencies = [ + "reqwest", + "thiserror", + "tokio", +] + +[[package]] +name = "google-cloud-storage" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac04b29849ebdeb9fb008988cc1c4d1f0c9d121b4c7f1ddeb8061df124580e93" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.21.2", + "bytes", + "futures-util", + "google-cloud-auth", + "google-cloud-metadata", + "google-cloud-token", + "hex", + "once_cell", + "percent-encoding", + "pkcs8", + "regex", + "reqwest", + "ring 0.17.7", + "serde", + "serde_json", + "sha2", + "thiserror", + "time 0.3.31", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "google-cloud-token" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c12ba8b21d128a2ce8585955246977fbce4415f680ebf9199b6f9d6d725f" +dependencies = [ + "async-trait", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -244,13 +601,19 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hashlink" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -259,6 +622,99 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -285,15 +741,30 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.10.5" @@ -318,6 +789,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.2", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -326,9 +811,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libsqlite3-sys" @@ -370,6 +855,16 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -380,10 +875,26 @@ dependencies = [ ] [[package]] -name = "matches" -version = "0.1.9" +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] [[package]] name = "miniz_oxide" @@ -395,6 +906,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -407,24 +949,106 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.10.0" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] [[package]] name = "pkg-config" @@ -432,6 +1056,12 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.63" @@ -450,6 +1080,87 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -459,19 +1170,33 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rusqlite" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags", + "bitflags 2.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -479,6 +1204,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustls" version = "0.21.7" @@ -486,19 +1217,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring", + "ring 0.16.20", "rustls-webpki", "sct", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.2", +] + [[package]] name = "rustls-webpki" version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -513,6 +1253,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scratch" version = "1.0.2" @@ -525,28 +1271,28 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "serde" -version = "1.0.147" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.32", ] [[package]] @@ -560,18 +1306,88 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.31", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strum" version = "0.25.0" @@ -588,7 +1404,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.22", + "syn 2.0.32", ] [[package]] @@ -604,15 +1420,36 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "taskchampion" version = "0.4.1" @@ -621,14 +1458,16 @@ dependencies = [ "byteorder", "chrono", "flate2", + "google-cloud-storage", "log", - "ring", + "ring 0.17.7", "rusqlite", "serde", "serde_json", "strum", "strum_macros", "thiserror", + "tokio", "ureq", "uuid", ] @@ -647,6 +1486,7 @@ dependencies = [ name = "tc-rust" version = "0.1.0" dependencies = [ + "taskchampion", "taskchampion-lib", ] @@ -689,6 +1529,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.5.1" @@ -704,11 +1573,122 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -718,9 +1698,9 @@ checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] @@ -737,13 +1717,19 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ - "base64", + "base64 0.21.2", "flate2", "log", "once_cell", @@ -755,21 +1741,26 @@ dependencies = [ [[package]] name = "url" -version = "2.2.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", "serde", @@ -787,11 +1778,20 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" @@ -818,6 +1818,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.79" @@ -847,6 +1859,19 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.56" @@ -893,3 +1918,151 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/src/tc/rust/Cargo.toml b/src/tc/rust/Cargo.toml index 8220b93b8..a3e7ca67c 100644 --- a/src/tc/rust/Cargo.toml +++ b/src/tc/rust/Cargo.toml @@ -6,4 +6,5 @@ version = "0.1.0" crate-type = ["staticlib"] [dependencies] -taskchampion-lib = {path = "../../../taskchampion/lib"} +taskchampion = { path = "../../../taskchampion/taskchampion", features = ["server-gcp", "server-sync"] } +taskchampion-lib = { path = "../../../taskchampion/lib" } diff --git a/taskchampion/docs/src/SUMMARY.md b/taskchampion/docs/src/SUMMARY.md index 9c7d87065..b5bc3c6e5 100644 --- a/taskchampion/docs/src/SUMMARY.md +++ b/taskchampion/docs/src/SUMMARY.md @@ -11,4 +11,7 @@ * [Synchronization Model](./sync-model.md) * [Snapshots](./snapshots.md) * [Server-Replica Protocol](./sync-protocol.md) + * [Encryption](./encryption.md) + * [HTTP Implementation](./http.md) + * [Object-Store Implementation](./object-store.md) * [Planned Functionality](./plans.md) diff --git a/taskchampion/docs/src/encryption.md b/taskchampion/docs/src/encryption.md new file mode 100644 index 000000000..d05900dae --- /dev/null +++ b/taskchampion/docs/src/encryption.md @@ -0,0 +1,38 @@ +# Encryption + +The client configuration includes an encryption secret of arbitrary length. +This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots). + +Encryption is not used for local (on-disk) sync, but is used for all cases where data is sent from the local host. + +## Key Derivation + +The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations. +The salt value depends on the implemenation of the protocol, as described in subsequent chapters. + +## Encryption + +The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305. +The client should generate a random nonce, noting that AEAD is _not secure_ if a nonce is used repeatedly for the same key. + +AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations. +In this protocol, the AAD is always 17 bytes of the form: + * `app_id` (byte) - always 1 + * `version_id` (16 bytes) - 16-byte form of the version ID associated with this data + * for versions (AddVersion, GetChildVersion), the _parent_ version_id + * for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id + +The `app_id` field is for future expansion to handle other, non-task data using this protocol. +Including it in the AAD ensures that such data cannot be confused with task data. + +Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's `EVP_AEAD_CTX_seal`. + +## Representation + +The final byte-stream is comprised of the following structure: + +* `version` (byte) - format version (always 1) +* `nonce` (12 bytes) - encryption nonce +* `ciphertext` (remaining bytes) - ciphertext from sealing operation + +The `version` field identifies this data format, and future formats will have a value other than 1 in this position. diff --git a/taskchampion/docs/src/http.md b/taskchampion/docs/src/http.md new file mode 100644 index 000000000..e0c504bc0 --- /dev/null +++ b/taskchampion/docs/src/http.md @@ -0,0 +1,65 @@ +# HTTP Representation + +The transactions in the sync protocol are realized for an HTTP server at `` using the HTTP requests and responses described here. +The `origin` *should* be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption. + +The replica identifies itself to the server using a `client_id` in the form of a UUID. +This value is passed with every request in the `X-Client-Id` header, in its dashed-hex format. + +The salt used in key derivation is the SHA256 hash of the 16-byte form of the client ID. + +## AddVersion + +The request is a `POST` to `/v1/client/add-version/`. +The request body contains the history segment, optionally encoded using any encoding supported by actix-web. +The content-type must be `application/vnd.taskchampion.history-segment`. + +The success response is a 200 OK with an empty body. +The new version ID appears in the `X-Version-Id` header. +If included, a snapshot request appears in the `X-Snapshot-Request` header with value `urgency=low` or `urgency=high`. + +On conflict, the response is a 409 CONFLICT with an empty body. +The expected parent version ID appears in the `X-Parent-Version-Id` header. + +Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification. + +## GetChildVersion + +The request is a `GET` to `/v1/client/get-child-version/`. + +The response is determined as described above. +The _not-found_ response is 404 NOT FOUND. +The _gone_ response is 410 GONE. +Neither has a response body. + +On success, the response is a 200 OK. +The version's history segment is returned in the response body, with content-type `application/vnd.taskchampion.history-segment`. +The version ID appears in the `X-Version-Id` header. +The response body may be encoded, in accordance with any `Accept-Encoding` header in the request. + +On failure, a client should treat a 404 NOT FOUND as indicating that it is up-to-date. +Clients should treat a 410 GONE as a synchronization error. +If the client has pending changes to send to the server, based on a now-removed version, then those changes cannot be reconciled and will be lost. +The client should, optionally after consulting the user, download and apply the latest snapshot. + +## AddSnapshot + +The request is a `POST` to `/v1/client/add-snapshot/`. +The request body contains the snapshot data, optionally encoded using any encoding supported by actix-web. +The content-type must be `application/vnd.taskchampion.snapshot`. + +If the version is invalid, as described above, the response should be 400 BAD REQUEST. +The server response should be 200 OK on success. + +## GetSnapshot + +The request is a `GET` to `/v1/client/snapshot`. + +The response is a 200 OK. +The snapshot is returned in the response body, with content-type `application/vnd.taskchampion.snapshot`. +The version ID appears in the `X-Version-Id` header. +The response body may be encoded, in accordance with any `Accept-Encoding` header in the request. + +After downloading and decrypting a snapshot, a client must replace its entire local task database with the content of the snapshot. +Any local operations that had not yet been synchronized must be discarded. +After the snapshot is applied, the client should begin the synchronization process again, starting from the snapshot version. diff --git a/taskchampion/docs/src/object-store.md b/taskchampion/docs/src/object-store.md new file mode 100644 index 000000000..03ff723a0 --- /dev/null +++ b/taskchampion/docs/src/object-store.md @@ -0,0 +1,8 @@ +# Object Store Representation + +TaskChampion also supports use of a generic key-value store to synchronize replicas. + +In this case, the salt used in key derivation is the SHA256 hash of the string "TaskChampion". + +The details of the mapping from this protocol to keys an values are private to the implementation. +Other applications should not access the key-value store directly. diff --git a/taskchampion/docs/src/snapshots.md b/taskchampion/docs/src/snapshots.md index 1e608dba3..1ca134f34 100644 --- a/taskchampion/docs/src/snapshots.md +++ b/taskchampion/docs/src/snapshots.md @@ -2,7 +2,7 @@ The basic synchronization model described in the previous page has a few shortcomings: * servers must store an ever-increasing quantity of versions - * a new replica must download all versions since the beginning in order to derive the current state + * a new replica must download all versions since the beginning (the nil UUID) in order to derive the current state Snapshots allow TaskChampion to avoid both of these issues. A snapshot is a copy of the task database at a specific version. @@ -37,12 +37,3 @@ This saves resources in these restricted environments. A snapshot must be made on a replica with no unsynchronized operations. As such, it only makes sense to request a snapshot in response to a successful AddVersion request. - -## Handling Deleted Versions - -When a replica requests a child version, the response must distinguish two cases: - - 1. No such child version exists because the replica is up-to-date. - 1. No such child version exists because it has been deleted, and the replica must re-initialize itself. - -The details of this logic are covered in the [Server-Replica Protocol](./sync-protocol.md). diff --git a/taskchampion/docs/src/sync-model.md b/taskchampion/docs/src/sync-model.md index 11d6e0855..f03626efb 100644 --- a/taskchampion/docs/src/sync-model.md +++ b/taskchampion/docs/src/sync-model.md @@ -32,7 +32,10 @@ For those familiar with distributed version control systems, a state is analogou Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create. Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on. The synchronization process uses operational transformation to "linearize" those operations. + This process is analogous (vaguely) to rebasing a sequence of Git commits. +Critically, though, operations cannot merge; in effect, the only option is rebasing. +Furthermore, once an operation has been sent to the server it cannot be changed; in effect, the server does not permit "force push". ### Sync Operations @@ -135,4 +138,4 @@ Without synchronization, its list of pending operations would grow indefinitely, So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically. TaskChampion provides a `LocalServer` for this purpose. -It implements the `get_child_version` and `add_version` operations as described, storing data on-disk locally, all within the `ta` binary. +It implements the `get_child_version` and `add_version` operations as described, storing data on-disk locally. diff --git a/taskchampion/docs/src/sync-protocol.md b/taskchampion/docs/src/sync-protocol.md index a67cd66be..30a472320 100644 --- a/taskchampion/docs/src/sync-protocol.md +++ b/taskchampion/docs/src/sync-protocol.md @@ -1,91 +1,42 @@ # Server-Replica Protocol -The server-replica protocol is defined abstractly in terms of request/response transactions from the replica to the server. -This is made concrete in an HTTP representation. +The server-replica protocol is defined abstractly in terms of request/response transactions. -The protocol builds on the model presented in the previous chapter, and in particular on the synchronization process. +The protocol builds on the model presented in the previous chapters, and in particular on the synchronization process. ## Clients -From the server's perspective, replicas accessing the same task history are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history. - -Each client is identified and authenticated with a "client_id key", known only to the server and to the replicas replicating the task history. +From the protocol's perspective, replicas accessing the same task history are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history. ## Server +A server implements the requests and responses described below. +Where the logic is implemented depends on the specific implementation of the protocol. + For each client, the server is responsible for storing the task history, in the form of a branch-free sequence of versions. It also stores the latest snapshot, if any exists. +From the server's perspective, snapshots and versions are opaque byte sequences. - * versions: a set of {versionId: UUID, parentVersionId: UUID, historySegment: bytes} - * latestVersionId: UUID - * snapshotVersionId: UUID - * snapshot: bytes - -For each client, it stores a set of versions as well as the latest version ID, defaulting to the nil UUID. -Each version has a version ID, a parent version ID, and a history segment (opaque data containing the operations for that version). -The server should maintain the following invariants for each client: +## Version Invariant -1. latestVersionId is nil or exists in the set of versions. -2. Given versions v1 and v2 for a client, with v1.versionId != v2.versionId and v1.parentVersionId != nil, v1.parentVersionId != v2.parentVersionId. - In other words, versions do not branch. -3. If snapshotVersionId is nil, then there is a version with parentVersionId == nil. -4. If snapshotVersionId is not nil, then there is a version with parentVersionId = snapshotVersionId. +The following invariant must always hold: -Note that versions form a linked list beginning with the latestVersionId stored for the client. -This linked list need not continue back to a version with v.parentVersionId = nil. -It may end at any point when v.parentVersionId is not found in the set of Versions. -This observation allows the server to discard older versions. -The third invariant prevents the server from discarding versions if there is no snapshot. -The fourth invariant prevents the server from discarding versions newer than the snapshot. +> All versions accessible from the server, when linked by parent-child relationships, must form a single chain. +> That is, each version must have no more than one parent and one child, and no more than one version may have zero parents or zero children. ## Data Formats -### Encryption - -The client configuration includes an encryption secret of arbitrary length and a clientId to identify itself. -This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots). - -#### Key Derivation - -The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations. -The salt is the SHA256 hash of the 16-byte form of the client ID. - -#### Encryption - -The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305. -The client should generate a random nonce, noting that AEAD is _not secure_ if a nonce is used repeatedly for the same key. - -AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations. -In this protocol, the AAD is always 17 bytes of the form: - * `app_id` (byte) - always 1 - * `version_id` (16 bytes) - 16-byte form of the version ID associated with this data - * for versions (AddVersion, GetChildVersion), the _parent_ version_id - * for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id - -The `app_id` field is for future expansion to handle other, non-task data using this protocol. -Including it in the AAD ensures that such data cannot be confused with task data. - -Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's `EVP_AEAD_CTX_seal`. - -#### Representation - -The final byte-stream is comprised of the following structure: - -* `version` (byte) - format version (always 1) -* `nonce` (12 bytes) - encryption nonce -* `ciphertext` (remaining bytes) - ciphertext from sealing operation - -The `version` field identifies this data format, and future formats will have a value other than 1 in this position. +All data sent to the server is encrypted by the client, using the scheme described in the "Encryption" chapter. ### Version The decrypted form of a version is a JSON array containing operations in the order they should be applied. Each operation has the form `{TYPE: DATA}`, for example: - * `{"Create":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}` - * `{"Delete":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}` - * `{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":"v","timestamp":"2021-10-11T12:47:07.188090948Z"}}` - * `{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":null,"timestamp":"2021-10-11T12:47:07.188090948Z"}}` (to delete a property) + * `[{"Create":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}]` + * `[{"Delete":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}]` + * `[{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":"v","timestamp":"2021-10-11T12:47:07.188090948Z"}}]` + * `[{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":null,"timestamp":"2021-10-11T12:47:07.188090948Z"}}]` (to delete a property) Timestamps are in RFC3339 format with a `Z` suffix. @@ -108,24 +59,25 @@ For example (pretty-printed for clarity): ## Transactions +All interactions between the client and server are defined in terms of request/response transactions, as described here. + ### AddVersion The AddVersion transaction requests that the server add a new version to the client's task history. The request contains the following; - * parent version ID - * history segment + * parent version ID, and + * encrypted version data. The server determines whether the new version is acceptable, atomically with respect to other requests for the same client. If it has no versions for the client, it accepts the version. -If it already has one or more versions for the client, then it accepts the version only if the given parent version ID matches its stored latest parent ID. +If it already has one or more versions for the client, then it accepts the version only if the given parent version has no children, thereby maintaining the version invariant. If the version is accepted, the server generates a new version ID for it. -The version is added to the set of versions for the client, the client's latest version ID is set to the new version ID. -The new version ID is returned in the response to the client. +The version is added to the chain of versions for the client, and the new version ID is returned in the response to the client. The response may also include a request for a snapshot, with associated urgency. -If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the latest version ID. +If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the ID of the version which has no children. The client may then "rebase" its operations and try again. Note that if a client receives two conflict responses with the same parent version ID, it is an indication that the client's version history has diverged from that on the server. @@ -138,23 +90,17 @@ If found, it returns the version's * version ID, * parent version ID (matching that in the request), and - * history segment. + * encrypted version data. -The response is either a version (success, _not-found_, or _gone_, as determined by the first of the following to apply: -* If a version with parentVersionId equal to the requested parentVersionId exists, it is returned. -* If the requested parentVersionId is the nil UUID .. - * ..and snapshotVersionId is nil, the response is _not-found_ (the client has no versions). - * ..and snapshotVersionId is not nil, the response is _gone_ (the first version has been deleted). -* If a version with versionId equal to the requested parentVersionId exists, the response is _not-found_ (the client is up-to-date) -* Otherwise, the response is _gone_ (the requested version has been deleted). +If not found, it returns an indication that no such version exists. ### AddSnapshot The AddSnapshot transaction requests that the server store a new snapshot, generated by the client. The request contains the following: - * version ID at which the snapshot was made - * snapshot data (opaque to the server) + * version ID at which the snapshot was made, and + * encrypted snapshot data. The server should validate that the snapshot is for an existing version and is newer than any existing snapshot. It may also validate that the snapshot is for a "recent" version (e.g., one of the last 5 versions). @@ -167,66 +113,3 @@ The server response is empty. The GetSnapshot transaction requests that the server provide the latest snapshot. The response contains the snapshot version ID and the snapshot data, if those exist. -## HTTP Representation - -The transactions above are realized for an HTTP server at `` using the HTTP requests and responses described here. -The `origin` *should* be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption. - -The replica identifies itself to the server using a `client_id` in the form of a UUID. -This value is passed with every request in the `X-Client-Id` header, in its dashed-hex format. - -### AddVersion - -The request is a `POST` to `/v1/client/add-version/`. -The request body contains the history segment, optionally encoded using any encoding supported by actix-web. -The content-type must be `application/vnd.taskchampion.history-segment`. - -The success response is a 200 OK with an empty body. -The new version ID appears in the `X-Version-Id` header. -If included, a snapshot request appears in the `X-Snapshot-Request` header with value `urgency=low` or `urgency=high`. - -On conflict, the response is a 409 CONFLICT with an empty body. -The expected parent version ID appears in the `X-Parent-Version-Id` header. - -Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification. - -### GetChildVersion - -The request is a `GET` to `/v1/client/get-child-version/`. - -The response is determined as described above. -The _not-found_ response is 404 NOT FOUND. -The _gone_ response is 410 GONE. -Neither has a response body. - -On success, the response is a 200 OK. -The version's history segment is returned in the response body, with content-type `application/vnd.taskchampion.history-segment`. -The version ID appears in the `X-Version-Id` header. -The response body may be encoded, in accordance with any `Accept-Encoding` header in the request. - -On failure, a client should treat a 404 NOT FOUND as indicating that it is up-to-date. -Clients should treat a 410 GONE as a synchronization error. -If the client has pending changes to send to the server, based on a now-removed version, then those changes cannot be reconciled and will be lost. -The client should, optionally after consulting the user, download and apply the latest snapshot. - -### AddSnapshot - -The request is a `POST` to `/v1/client/add-snapshot/`. -The request body contains the snapshot data, optionally encoded using any encoding supported by actix-web. -The content-type must be `application/vnd.taskchampion.snapshot`. - -If the version is invalid, as described above, the response should be 400 BAD REQUEST. -The server response should be 200 OK on success. - -### GetSnapshot - -The request is a `GET` to `/v1/client/snapshot`. - -The response is a 200 OK. -The snapshot is returned in the response body, with content-type `application/vnd.taskchampion.snapshot`. -The version ID appears in the `X-Version-Id` header. -The response body may be encoded, in accordance with any `Accept-Encoding` header in the request. - -After downloading and decrypting a snapshot, a client must replace its entire local task database with the content of the snapshot. -Any local operations that had not yet been synchronized must be discarded. -After the snapshot is applied, the client should begin the synchronization process again, starting from the snapshot version. diff --git a/taskchampion/integration-tests/src/bindings_tests/replica.c b/taskchampion/integration-tests/src/bindings_tests/replica.c index 0fc0c1f54..7ea2a02a5 100644 --- a/taskchampion/integration-tests/src/bindings_tests/replica.c +++ b/taskchampion/integration-tests/src/bindings_tests/replica.c @@ -185,7 +185,7 @@ static void test_replica_sync_local(void) { // When tc_replica_undo is passed NULL for undone_out, it still succeeds static void test_replica_remote_server(void) { TCString err; - TCServer *server = tc_server_new_remote( + TCServer *server = tc_server_new_sync( tc_string_borrow("tc.freecinc.com"), tc_uuid_new_v4(), tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8 diff --git a/taskchampion/lib/src/server.rs b/taskchampion/lib/src/server.rs index f8823ceac..a043c1fcb 100644 --- a/taskchampion/lib/src/server.rs +++ b/taskchampion/lib/src/server.rs @@ -108,13 +108,13 @@ pub unsafe extern "C" fn tc_server_new_local( /// The server must be freed after it is used - tc_replica_sync does not automatically free it. /// /// ```c -/// EXTERN_C struct TCServer *tc_server_new_remote(struct TCString origin, +/// EXTERN_C struct TCServer *tc_server_new_sync(struct TCString origin, /// struct TCUuid client_id, /// struct TCString encryption_secret, /// struct TCString *error_out); /// ``` #[no_mangle] -pub unsafe extern "C" fn tc_server_new_remote( +pub unsafe extern "C" fn tc_server_new_sync( origin: TCString, client_id: TCUuid, encryption_secret: TCString, @@ -129,8 +129,8 @@ pub unsafe extern "C" fn tc_server_new_remote( // SAFETY: // - client_id is a valid Uuid (any 8-byte sequence counts) - let client_id = unsafe { TCUuid::val_from_arg(client_id) }; + // SAFETY: // - encryption_secret is valid (promised by caller) // - encryption_secret ownership is transferred to this function @@ -154,6 +154,54 @@ pub unsafe extern "C" fn tc_server_new_remote( #[ffizz_header::item] #[ffizz(order = 802)] +/// Create a new TCServer that connects to the Google Cloud Platform. See the TaskChampion docs +/// for the description of the arguments. +/// +/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +/// returned. The caller must free this string. +/// +/// The server must be freed after it is used - tc_replica_sync does not automatically free it. +/// +/// ```c +/// EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, +/// struct TCString encryption_secret, +/// struct TCString *error_out); +/// ``` +#[no_mangle] +pub unsafe extern "C" fn tc_server_new_gcp( + bucket: TCString, + encryption_secret: TCString, + error_out: *mut TCString, +) -> *mut TCServer { + wrap( + || { + // SAFETY: + // - bucket is valid (promised by caller) + // - bucket ownership is transferred to this function + let bucket = unsafe { TCString::val_from_arg(bucket) }.into_string()?; + + // SAFETY: + // - encryption_secret is valid (promised by caller) + // - encryption_secret ownership is transferred to this function + let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) } + .as_bytes() + .to_vec(); + + let server_config = ServerConfig::Gcp { + bucket, + encryption_secret, + }; + let server = server_config.into_server()?; + // SAFETY: caller promises to free this server. + Ok(unsafe { TCServer::return_ptr(server.into()) }) + }, + error_out, + std::ptr::null_mut(), + ) +} + +#[ffizz_header::item] +#[ffizz(order = 899)] /// Free a server. The server may not be used after this function returns and must not be freed /// more than once. /// diff --git a/taskchampion/lib/taskchampion.h b/taskchampion/lib/taskchampion.h index 82cabdd8e..eb7191739 100644 --- a/taskchampion/lib/taskchampion.h +++ b/taskchampion/lib/taskchampion.h @@ -432,11 +432,22 @@ EXTERN_C struct TCServer *tc_server_new_local(struct TCString server_dir, struct // returned. The caller must free this string. // // The server must be freed after it is used - tc_replica_sync does not automatically free it. -EXTERN_C struct TCServer *tc_server_new_remote(struct TCString origin, +EXTERN_C struct TCServer *tc_server_new_sync(struct TCString origin, struct TCUuid client_id, struct TCString encryption_secret, struct TCString *error_out); +// Create a new TCServer that connects to the Google Cloud Platform. See the TaskChampion docs +// for the description of the arguments. +// +// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +// returned. The caller must free this string. +// +// The server must be freed after it is used - tc_replica_sync does not automatically free it. +EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, + struct TCString encryption_secret, + struct TCString *error_out); + // Free a server. The server may not be used after this function returns and must not be freed // more than once. EXTERN_C void tc_server_free(struct TCServer *server); diff --git a/taskchampion/taskchampion/Cargo.toml b/taskchampion/taskchampion/Cargo.toml index 7a875fbc7..869148ece 100644 --- a/taskchampion/taskchampion/Cargo.toml +++ b/taskchampion/taskchampion/Cargo.toml @@ -12,9 +12,16 @@ edition = "2021" rust-version = "1.65" [features] -default = ["server-sync" ] -server-sync = ["crypto", "dep:ureq"] -crypto = ["dep:ring"] +default = ["server-sync", "server-gcp"] + +# Support for sync to a server +server-sync = ["encryption", "dep:ureq"] +# Support for sync to GCP +server-gcp = ["cloud", "encryption", "dep:google-cloud-storage", "dep:tokio"] +# (private) Support for sync protocol encryption +encryption = ["dep:ring"] +# (private) Generic support for cloud sync +cloud = [] [package.metadata.docs.rs] all-features = true @@ -34,7 +41,11 @@ strum_macros.workspace = true flate2.workspace = true byteorder.workspace = true ring.workspace = true +google-cloud-storage.workspace = true +tokio.workspace = true +google-cloud-storage.optional = true +tokio.optional = true ureq.optional = true ring.optional = true diff --git a/taskchampion/taskchampion/src/errors.rs b/taskchampion/taskchampion/src/errors.rs index eab0b71c9..031c6ce93 100644 --- a/taskchampion/taskchampion/src/errors.rs +++ b/taskchampion/taskchampion/src/errors.rs @@ -40,5 +40,9 @@ other_error!(io::Error); other_error!(serde_json::Error); other_error!(rusqlite::Error); other_error!(crate::storage::sqlite::SqliteError); +#[cfg(feature = "server-gcp")] +other_error!(google_cloud_storage::http::Error); +#[cfg(feature = "server-gcp")] +other_error!(google_cloud_storage::client::google_cloud_auth::error::Error); pub type Result = std::result::Result; diff --git a/taskchampion/taskchampion/src/lib.rs b/taskchampion/taskchampion/src/lib.rs index 7cc7c20fa..fefdf6126 100644 --- a/taskchampion/taskchampion/src/lib.rs +++ b/taskchampion/taskchampion/src/lib.rs @@ -40,6 +40,7 @@ Support for some optional functionality is controlled by feature flags. Sync server client support: + * `server-gcp` - sync to Google Cloud Platform * `server-sync` - sync to the taskchampion-sync-server # See Also diff --git a/taskchampion/taskchampion/src/server/cloud/gcp.rs b/taskchampion/taskchampion/src/server/cloud/gcp.rs new file mode 100644 index 000000000..33f409754 --- /dev/null +++ b/taskchampion/taskchampion/src/server/cloud/gcp.rs @@ -0,0 +1,395 @@ +#![allow(unused_variables, dead_code)] +use super::service::{Service, ObjectInfo}; +use crate::errors::Result; +use google_cloud_storage::client::{Client, ClientConfig}; +use google_cloud_storage::http::error::ErrorResponse; +use google_cloud_storage::http::Error as GcsError; +use google_cloud_storage::http::{self, objects}; +use tokio::runtime::Runtime; + +/// A [`Service`] implementation based on the Google Cloud Storage service. +pub(in crate::server) struct GcpService { + client: Client, + rt: Runtime, + bucket: String, +} + +/// Determine whether the given result contains an HTTP error with the given code. +fn is_http_error(query: u16, res: &std::result::Result) -> bool { + match res { + // Errors from RPC's. + Err(GcsError::Response(ErrorResponse { code, .. })) => *code == query, + // Errors from reqwest (downloads, uploads). + Err(GcsError::HttpClient(e)) => e.status().map(|s| s.as_u16()) == Some(query), + _ => false, + } +} + +impl GcpService { + pub(in crate::server) fn new(bucket: String) -> Result { + let rt = Runtime::new()?; + let config = rt.block_on( + ClientConfig::default().with_auth(), + )?; + Ok(Self { + client: Client::new(config), + rt, + bucket, + }) + } +} + +impl Service for GcpService { + fn put(&mut self, name: &[u8], value: &[u8]) -> Result<()> { + let name = String::from_utf8(name.to_vec()).expect("non-UTF8 object name"); + let upload_type = objects::upload::UploadType::Simple(objects::upload::Media::new(name)); + self.rt.block_on(self.client.upload_object( + &objects::upload::UploadObjectRequest { + bucket: self.bucket.clone(), + ..Default::default() + }, + value.to_vec(), + &upload_type, + ))?; + Ok(()) + } + + fn get(&mut self, name: &[u8]) -> Result>> { + let name = String::from_utf8(name.to_vec()).expect("non-UTF8 object name"); + let download_res = self.rt.block_on(self.client.download_object( + &objects::get::GetObjectRequest { + bucket: self.bucket.clone(), + object: name, + ..Default::default() + }, + &objects::download::Range::default(), + )); + if is_http_error(404, &download_res) { + Ok(None) + } else { + Ok(Some(download_res?)) + } + } + + fn del(&mut self, name: &[u8]) -> Result<()> { + let name = String::from_utf8(name.to_vec()).expect("non-UTF8 object name"); + let del_res = self.rt.block_on(self.client.delete_object( + &objects::delete::DeleteObjectRequest { + bucket: self.bucket.clone(), + object: name, + ..Default::default() + }, + )); + if !is_http_error(404, &del_res) { + del_res?; + } + Ok(()) + } + + fn list<'a>(&'a mut self, prefix: &[u8]) -> Box> + 'a> { + let prefix = String::from_utf8(prefix.to_vec()).expect("non-UTF8 object prefix"); + Box::new(ObjectIterator { + service: self, + prefix, + last_response: None, + next_index: 0, + }) + } + + fn compare_and_swap( + &mut self, + name: &[u8], + existing_value: Option>, + new_value: Vec, + ) -> Result { + let name = String::from_utf8(name.to_vec()).expect("non-UTF8 object name"); + let get_res = self + .rt + .block_on(self.client.get_object(&objects::get::GetObjectRequest { + bucket: self.bucket.clone(), + object: name.clone(), + ..Default::default() + })); + let generation; + if is_http_error(404, &get_res) { + // If a value was expected, that expectation has not been met. + if existing_value.is_some() { + return Ok(false); + } + // Generation 0 indicates that the upload should succeed only if the object does not + // exist. + generation = 0; + } else { + generation = get_res?.generation; + } + + // If the file existed, then verify its contents. + if generation > 0 { + let data = self.rt.block_on(self.client.download_object( + &objects::get::GetObjectRequest { + bucket: self.bucket.clone(), + object: name.clone(), + // Fetch the same generation. + generation: Some(generation), + ..Default::default() + }, + &objects::download::Range::default(), + ))?; + if Some(data) != existing_value { + return Ok(false); + } + } + + // Finally, put the new value with a condition that the generation hasn't changed. + let upload_type = objects::upload::UploadType::Simple(objects::upload::Media::new(name)); + let upload_res = self.rt.block_on(self.client.upload_object( + &objects::upload::UploadObjectRequest { + bucket: self.bucket.clone(), + if_generation_match: Some(generation), + ..Default::default() + }, + new_value.to_vec(), + &upload_type, + )); + if is_http_error(412, &upload_res) { + // A 412 indicates the precondition was not satisfied: the given generation + // is no longer the latest. + Ok(false) + } else { + upload_res?; + Ok(true) + } + } +} + +/// An Iterator returning names of objects from `list_objects`. +/// +/// This handles response pagination by fetching one page at a time. +struct ObjectIterator<'a> { + service: &'a mut GcpService, + prefix: String, + last_response: Option, + next_index: usize, +} + +impl<'a> ObjectIterator<'a> { + fn fetch_batch(&mut self) -> Result<()> { + let mut page_token = None; + if let Some(ref resp) = self.last_response { + page_token = resp.next_page_token.clone(); + } + self.last_response = Some(self.service.rt.block_on(self.service.client.list_objects( + &objects::list::ListObjectsRequest { + bucket: self.service.bucket.clone(), + prefix: Some(self.prefix.clone()), + page_token, + #[cfg(test)] // For testing, use a small page size. + max_results: Some(6), + ..Default::default() + }, + ))?); + self.next_index = 0; + Ok(()) + } +} + +impl<'a> Iterator for ObjectIterator<'a> { + type Item = Result; + fn next(&mut self) -> Option { + // If the iterator is just starting, fetch the first response. + if self.last_response.is_none() { + if let Err(e) = self.fetch_batch() { + return Some(Err(e.into())); + } + } + if let Some(ref result) = self.last_response { + if let Some(ref items) = result.items { + if self.next_index < items.len() { + // Return a result from the existing response. + let obj = &items[self.next_index]; + self.next_index += 1; + let creation = obj.time_created.map(|t| t.unix_timestamp()).unwrap_or(0); + let creation: u64 = creation.try_into().unwrap_or(0); + return Some(Ok(ObjectInfo { + name: obj.name.as_bytes().to_vec(), + creation, + })); + } else if result.next_page_token.is_some() { + // Fetch the next page and try again. + if let Err(e) = self.fetch_batch() { + return Some(Err(e.into())); + } + return self.next(); + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + /// Make a service if `GCP_TEST_BUCKET` is set, as well as a function to put a unique prefix on + /// an object name, so that tests do not interfere with one another. + /// + /// Set up this bucket with a lifecyle policy to delete objects with age > 1 day. While passing + /// tests should correctly clean up after themselves, failing tests may leave objects in the + /// bucket. + /// + /// When the environment variable is not set, this returns false and the test does not run. + /// Note that the Rust test runner will still show "ok" for the test, as there is no way to + /// indicate anything else. + fn make_service() -> Option<(GcpService, impl Fn(&str) -> Vec)> { + let Ok(bucket) = std::env::var("GCP_TEST_BUCKET") else { + return None; + }; + let prefix = Uuid::new_v4(); + Some((GcpService::new(bucket).unwrap(), move |n: &_| { + format!("{}-{}", prefix.as_simple(), n).into_bytes() + })) + } + + #[test] + fn put_and_get() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + svc.put(&pfx("testy"), b"foo").unwrap(); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, Some(b"foo".to_vec())); + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } + + #[test] + fn get_missing() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, None); + } + + #[test] + fn del() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + svc.put(&pfx("testy"), b"data").unwrap(); + svc.del(&pfx("testy")).unwrap(); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, None); + } + + #[test] + fn del_missing() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } + + #[test] + fn list() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + let mut names: Vec<_> = (0..20).map(|i| pfx(&format!("pp-{i:02}"))).collect(); + names.sort(); + // Create 20 objects that will be listed. + for n in &names { + svc.put(n, b"data").unwrap(); + } + // And another object that should not be included in the list. + svc.put(&pfx("xxx"), b"data").unwrap(); + + let got_names: Vec<_> = svc.list(&pfx("pp-")).collect::>().unwrap(); + let mut got_names: Vec<_> = got_names.into_iter().map(|oi| oi.name).collect(); + got_names.sort(); + assert_eq!(got_names, names); + + // Clean up. + for n in got_names { + svc.del(&n).unwrap(); + } + svc.del(&pfx("xxx")).unwrap(); + } + + #[test] + fn compare_and_swap_create() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + + assert!(svc + .compare_and_swap(&pfx("testy"), None, b"bar".into()) + .unwrap()); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, Some(b"bar".to_vec())); + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } + + #[test] + fn compare_and_swap_matches() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + + // Create the existing file, with two generations. + svc.put(&pfx("testy"), b"foo1").unwrap(); + svc.put(&pfx("testy"), b"foo2").unwrap(); + assert!(svc + .compare_and_swap(&pfx("testy"), Some(b"foo2".into()), b"bar".into()) + .unwrap()); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, Some(b"bar".to_vec())); + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } + + #[test] + fn compare_and_swap_expected_no_file() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + + // Create the existing file, with two generations. + svc.put(&pfx("testy"), b"foo1").unwrap(); + assert!(!svc + .compare_and_swap(&pfx("testy"), None, b"bar".into()) + .unwrap()); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, Some(b"foo1".to_vec())); + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } + + #[test] + fn compare_and_swap_mismatch() { + let Some((mut svc, pfx)) = make_service() else { + return; + }; + + // Create the existing file, with two generations. + svc.put(&pfx("testy"), b"foo1").unwrap(); + svc.put(&pfx("testy"), b"foo2").unwrap(); + assert!(!svc + .compare_and_swap(&pfx("testy"), Some(b"foo1".into()), b"bar".into()) + .unwrap()); + let got = svc.get(&pfx("testy")).unwrap(); + assert_eq!(got, Some(b"foo2".to_vec())); + + // Clean up. + svc.del(&pfx("testy")).unwrap(); + } +} diff --git a/taskchampion/taskchampion/src/server/cloud/mod.rs b/taskchampion/taskchampion/src/server/cloud/mod.rs new file mode 100644 index 000000000..970ced75c --- /dev/null +++ b/taskchampion/taskchampion/src/server/cloud/mod.rs @@ -0,0 +1,16 @@ +/*! +* Support for cloud-service-backed sync. +* +* All of these operate using a similar approach, with specific patterns of object names. The +* process of adding a new version requires a compare-and-swap operation that sets a new version +* as the "latest" only if the existing "latest" has the expected value. This ensures a continuous +* chain of versions, even if multiple replicas attempt to sync at the same time. +*/ + +mod server; +mod service; + +pub(in crate::server) use server::CloudServer; + +#[cfg(feature = "server-gcp")] +pub(in crate::server) mod gcp; diff --git a/taskchampion/taskchampion/src/server/cloud/server.rs b/taskchampion/taskchampion/src/server/cloud/server.rs new file mode 100644 index 000000000..2d395de7b --- /dev/null +++ b/taskchampion/taskchampion/src/server/cloud/server.rs @@ -0,0 +1,1122 @@ +use super::service::{Service, ObjectInfo}; +#[cfg(not(test))] +use std::time::{SystemTime, UNIX_EPOCH}; +use crate::errors::{Error, Result}; +use crate::server::crypto::{Cryptor, Sealed, Unsealed}; +use crate::server::{ + AddVersionResult, GetVersionResult, HistorySegment, Server, Snapshot, SnapshotUrgency, + VersionId, +}; +use ring::rand; +use std::collections::{HashMap, HashSet}; +use uuid::Uuid; + +/// Implement the Server trait for a cloud service implemented by [`Service`]. +/// +/// This type implements a TaskChampion server over a basic object-storage service. It encapsulates +/// all of the logic to ensure a linear sequence of versions, encrypt and decrypt data, and clean +/// up old data so that this can be supported on a variety of cloud services. +/// +/// ## Encryption +/// +/// The encryption scheme is described in `sync-protocol.md`. The salt value used for key +/// derivation is the SHA256 hash of the string "TaskChampion". Object names are not encrypted, +/// by the nature of key/value stores. Since the content of the "latest" object can usually be +/// inferred from object names, it, too, is not encrypted. +/// +/// ## Object Organization +/// +/// UUIDs emebedded in names and values appear in their "simple" form: lower-case hexadecimal with +/// no hyphens. +/// +/// Versions are stored as objects with name `v-PARENT-VERSION` where `PARENT` is the parent +/// version's UUID and `VERSION` is the version's UUID. The object value is the raw history +/// segment. These objects are created with simple `put` requests, as the name uniquely identifies +/// the content. +/// +/// The latest version is stored as an object with name "latest", containing the UUID of the latest +/// version. This file is updated with `compare_and_swap`. After a successful update of this +/// object, the version is considered committed. +/// +/// Since there are no strong constraints on creation of version objects, it is possible +/// to have multiple such files with the same `PARENT`. However, only one such object will be +/// contained in the chain of parent-child relationships beginning with the value in "latest". +/// All other objects are invalid and not visible outside this type. +/// +/// Snapshots are stored as objects with name `s-VERSION` where `VERSION` is the version at which +/// the snapshot was made. These objects are created with simple `put` requests, as any snapshot +/// for a given version is interchangeable with any other. +/// +/// ## Cleanup +/// +/// Cleanup of unnecessary data is performed probabalistically after `add_version`, although any +/// errors are ignored. +/// +/// - Any versions not reachable from "latest" and which cannot become "latest" are deleted. +/// - Any snapshots older than the most recent are deleted. +/// - Any versions older than [`MAX_VERSION_AGE_SECS`] which are incorporated into a snapshot +/// are deleted. +pub(in crate::server) struct CloudServer { + service: SVC, + + /// The Cryptor supporting encryption and decryption of objects in this server. + cryptor: Cryptor, + + /// The probability (0..255) that this run will perform cleanup. + cleanup_probability: u8, + + /// For testing, a function that is called in the middle of `add_version` to simulate + /// a concurrent change in the service. + #[cfg(test)] + add_version_intercept: Option, +} + +const LATEST: &[u8] = b"latest"; +const DEFAULT_CLEANUP_PROBABILITY: u8 = 13; // about 5% + +#[cfg(not(test))] +const MAX_VERSION_AGE_SECS: u64 = 3600 * 24 * 180; // about half a year + +fn version_to_bytes(v: VersionId) -> Vec { + v.as_simple().to_string().into_bytes() +} + +impl CloudServer { + pub(in crate::server) fn new(service: SVC, encryption_secret: Vec) -> Result { + let cryptor = Cryptor::new(b"Taskchampion", &encryption_secret.into())?; + Ok(Self { + service, + cryptor, + cleanup_probability: DEFAULT_CLEANUP_PROBABILITY, + #[cfg(test)] + add_version_intercept: None, + }) + } + + /// Generate an object name for the given parent and child versions. + fn version_name(parent_version_id: &VersionId, child_version_id: &VersionId) -> Vec { + format!( + "v-{}-{}", + parent_version_id.as_simple(), + child_version_id.as_simple() + ) + .into_bytes() + } + + /// Parse a version name as generated by `version_name`. + fn parse_version_name(name: &[u8]) -> Option<(VersionId, VersionId)> { + if name.len() != 2 + 32 + 1 + 32 || !name.starts_with(b"v-") || name[2 + 32] != b'-' { + return None; + } + let Ok(parent_version_id) = VersionId::try_parse_ascii(&name[2..2 + 32]) else { + return None; + }; + let Ok(child_version_id) = VersionId::try_parse_ascii(&name[2 + 32 + 1..]) else { + return None; + }; + Some((parent_version_id, child_version_id)) + } + + /// Generate an object name for a snapshot at the given version. + fn snapshot_name(version_id: &VersionId) -> Vec { + format!("s-{}", version_id.as_simple()).into_bytes() + } + + /// Parse a snapshot name as generated by `snapshot_name`. + fn parse_snapshot_name(name: &[u8]) -> Option { + if name.len() != 2 + 32 || !name.starts_with(b"s-") { + return None; + } + let Ok(version_id) = VersionId::try_parse_ascii(&name[2..2 + 32]) else { + return None; + }; + Some(version_id) + } + + /// Generate a random integer in (0..255) for use in probabalistic decisions. + fn randint(&self) -> Result { + use rand::SecureRandom; + let mut randint = [0u8]; + rand::SystemRandom::new() + .fill(&mut randint) + .map_err(|_| Error::Server("Random number generator failure".into()))?; + Ok(randint[0]) + } + + /// Get the version from "latest", or None if the object does not exist. This always fetches a fresh + /// value from storage. + fn get_latest(&mut self) -> Result> { + let Some(latest) = self.service.get(LATEST)? else { + return Ok(None); + }; + let latest = VersionId::try_parse_ascii(&latest) + .map_err(|_| Error::Server("'latest' object contains invalid data".into()))?; + Ok(Some(latest)) + } + + /// Get the possible child versions of the given parent version, based only on the object + /// names. + fn get_child_versions(&mut self, parent_version_id: &VersionId) -> Result> { + Ok(self + .service + .list(format!("v-{}-", parent_version_id.as_simple()).as_bytes()) + .filter_map(|res| match res { + Ok(ObjectInfo { name, .. }) => { + if let Some((_, c)) = Self::parse_version_name(&name) { + Some(Ok(c)) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + .collect::>>()?) + } + + /// Determine the snapshot urgency. This is done probabalistically, so that approximately one + /// in 25 sync's results in a new snapshot on a client that is not trying to avoid snapshots, + /// and one in 100 on a client that is trying to avoid snapshots. + fn snapshot_urgency(&self) -> Result { + let r = self.randint()?; + if r < 2 { + Ok(SnapshotUrgency::High) + } else if r < 25 { + Ok(SnapshotUrgency::Low) + } else { + Ok(SnapshotUrgency::None) + } + } + + /// Maybe call `cleanup` depending on `cleanup_probability`. + fn maybe_cleanup(&mut self) -> Result<()> { + if self.randint()? < self.cleanup_probability { + self.cleanup_probability = DEFAULT_CLEANUP_PROBABILITY; + self.cleanup() + } else { + Ok(()) + } + } + + /// Perform cleanup, deleting unnecessary data. + fn cleanup(&mut self) -> Result<()> { + // Construct a vector containing all (child, parent, creation) tuples + let mut versions = self + .service + .list(b"v-") + .filter_map(|res| match res { + Ok(ObjectInfo { name, creation }) => { + if let Some((p, c)) = Self::parse_version_name(&name) { + Some(Ok((c, p, creation))) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + .collect::>>()?; + versions.sort(); + + // Function to find the parent of a given child version in `versions`, taking + // advantage of having sorted the vector by child version ID. + let parent_of = |c| match versions.binary_search_by_key(&c, |tup| tup.0) { + Ok(idx) => Some(versions[idx].1), + Err(_) => None, + }; + + // Create chains mapping forward (parent -> child) and backward (child -> parent), starting + // at "latest". + let mut fwd_chain = HashMap::new(); + let mut rev_chain = HashMap::new(); + let mut iterations = versions.len() + 1; // For cycle detection. + let latest = self.get_latest()?; + if let Some(mut c) = latest { + loop { + match parent_of(c) { + Some(p) => { + fwd_chain.insert(p, c); + rev_chain.insert(c, p); + c = p; + iterations -= 1; + if iterations == 0 { + return Err(Error::Server("Version cycle detected".into())); + } + } + None => break, + } + } + } + + // Collect all versions older than MAX_VERSION_AGE_SECS + #[cfg(not(test))] + let age_threshold = { + let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|t| t.as_secs()).unwrap_or(0); + now.saturating_sub(MAX_VERSION_AGE_SECS) + }; + + // In testing, cutoff age is 1000. + #[cfg(test)] + let age_threshold = 1000; + + let old_versions: HashSet = versions.iter().filter_map(|(c, _, creation)| { + if *creation < age_threshold { + Some(*c) + } else { + None + } + }).collect(); + + // Now, any pair not present in that chain can be deleted. However, another replica + // may be in the state where it has uploaded a version but not changed "latest" yet, + // so any pair with parent equal to latest is allowed to stay. + for (c, p, _) in versions { + if fwd_chain.get(&p) != Some(&c) && Some(p) != latest { + self.service.del(&Self::version_name(&p, &c))?; + } + } + + // Collect a set of all snapshots. + let snapshots = self.service.list(b"s-").filter_map(|res| match res { + Ok(ObjectInfo { name , .. }) => { + if let Some(v) = Self::parse_snapshot_name(&name) { + Some(Ok(v)) + } else { + None + } + } + Err(e) => Some(Err(e)), + }).collect::>>()?; + + // Find the latest snapshot by iterating back from "latest". Note that this iteration is + // guaranteed not to be cyclical, as that was checked above. + let mut latest_snapshot = None; + if let Some(mut version) = latest { + loop { + if snapshots.contains(&version) { + latest_snapshot = Some(version); + break; + } + if let Some(v) = rev_chain.get(&version) { + version = *v; + } else { + break; + } + } + } + + // If there's a latest snapshot, delete all other snapshots. + let Some(latest_snapshot) = latest_snapshot else { + // If there's no snapshot, no further cleanup is possible. + return Ok(()); + }; + for version in snapshots { + if version != latest_snapshot { + self.service.del(&Self::snapshot_name(&version))?; + } + } + + // Now continue iterating backward from that version; any version in `old_versions` can be + // deleted. + let mut version = latest_snapshot; + loop { + if let Some(parent) = rev_chain.get(&version) { + if old_versions.contains(&version) { + self.service.del(&Self::version_name(parent, &version))?; + } + version = *parent; + } else { + break; + } + } + + Ok(()) + } +} + +impl Server for CloudServer { + fn add_version( + &mut self, + parent_version_id: VersionId, + history_segment: HistorySegment, + ) -> Result<(AddVersionResult, SnapshotUrgency)> { + let latest = self.get_latest()?; + if let Some(l) = latest { + if l != parent_version_id { + return Ok(( + AddVersionResult::ExpectedParentVersion(l), + self.snapshot_urgency()?, + )); + } + } + + // Invent a new version ID and upload the version data. + let version_id = VersionId::new_v4(); + let new_name = Self::version_name(&parent_version_id, &version_id); + let sealed = self.cryptor.seal(Unsealed { + version_id, + payload: history_segment.into(), + })?; + self.service.put(&new_name, sealed.as_ref())?; + + #[cfg(test)] + if let Some(f) = self.add_version_intercept { + f(&mut self.service); + } + + // Try to compare-and-swap this value into LATEST + let old_value = latest.map(|l| version_to_bytes(l)); + let new_value = version_to_bytes(version_id); + if !self + .service + .compare_and_swap(LATEST, old_value, new_value)? + { + // Delete the version data, since it was not latest. + self.service.del(&new_name)?; + let latest = self.get_latest()?; + let latest = latest.unwrap_or(Uuid::nil()); + return Ok(( + AddVersionResult::ExpectedParentVersion(latest), + self.snapshot_urgency()?, + )); + } + + // Attempt a cleanup, but ignore errors. + let _ = self.maybe_cleanup(); + + return Ok((AddVersionResult::Ok(version_id), self.snapshot_urgency()?)); + } + + fn get_child_version(&mut self, parent_version_id: VersionId) -> Result { + // The `get_child_versions` function will usually return only one child version for a + // parent, in which case the work is easy. Otherwise, if there are several possible + // children, only one of those will lead to `latest`, and importantly the others will not + // have their own children. So we can detect the "true" child as the one that is equal to + // "latest" or has children. + let version_id = match &(self.get_child_versions(&parent_version_id)?)[..] { + [] => return Ok(GetVersionResult::NoSuchVersion), + [child] => *child, + children => { + // There are some extra version objects, so a cleanup is warranted. + self.cleanup_probability = 255; + let latest = self.get_latest()?; + let mut true_child = None; + for child in children { + if Some(*child) == latest { + true_child = Some(*child); + break; + } + } + if true_child.is_none() { + for child in children { + if self.get_child_versions(&child)?.len() > 0 { + true_child = Some(*child) + } + } + } + match true_child { + Some(true_child) => true_child, + None => return Ok(GetVersionResult::NoSuchVersion), + } + } + }; + + let Some(sealed) = self + .service + .get(&Self::version_name(&parent_version_id, &version_id))? + else { + // This really shouldn't happen, since the chain was derived from object names, but + // perhaps the object was deleted. + return Ok(GetVersionResult::NoSuchVersion); + }; + let unsealed = self.cryptor.unseal(Sealed { + version_id, + payload: sealed, + })?; + Ok(GetVersionResult::Version { + version_id, + parent_version_id, + history_segment: unsealed.into(), + }) + } + + fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> Result<()> { + let name = Self::snapshot_name(&version_id); + let sealed = self.cryptor.seal(Unsealed { + version_id, + payload: snapshot.into(), + })?; + self.service.put(&name, sealed.as_ref())?; + Ok(()) + } + + fn get_snapshot(&mut self) -> Result> { + // Pick the first snapshot we find. + let Some(name) = self.service.list(b"s-").nth(0) else { + return Ok(None); + }; + let ObjectInfo { name, .. } = name?; + let Some(version_id) = Self::parse_snapshot_name(&name) else { + return Ok(None); + }; + let Some(payload) = self.service.get(&name)? else { + return Ok(None); + }; + let unsealed = self.cryptor.unseal(Sealed { + version_id, + payload: payload.into(), + })?; + Ok(Some((version_id, unsealed.payload))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::server::NIL_VERSION_ID; + + /// A simple in-memory service for testing. All insertions via Service methods occur at time + /// `INSERTION_TIME`. All versions older that 1000 are considered "old". + #[derive(Default)] + struct MockService(HashMap, (u64, Vec)>); + + const INSERTION_TIME: u64 = 9999999999; + + impl Service for MockService { + fn put(&mut self, name: &[u8], value: &[u8]) -> Result<()> { + self.0.insert(name.to_vec(), (INSERTION_TIME, value.to_vec())); + Ok(()) + } + + fn get(&mut self, name: &[u8]) -> Result>> { + Ok(self.0.get(name).map(|(_, data)| data.clone())) + } + + fn del(&mut self, name: &[u8]) -> Result<()> { + self.0.remove(name); + Ok(()) + } + + fn compare_and_swap( + &mut self, + name: &[u8], + existing_value: Option>, + new_value: Vec, + ) -> Result { + if self.0.get(name).map(|(_, d)| d) == existing_value.as_ref() { + self.0.insert(name.to_vec(), (INSERTION_TIME, new_value)); + return Ok(true); + } + Ok(false) + } + + fn list<'a>(&'a mut self, prefix: &[u8]) -> Box> + 'a> { + let prefix = prefix.to_vec(); + Box::new( + self.0 + .iter() + .filter(move |(k, _)| k.starts_with(&prefix)) + .map(|(k, (t, _))| Ok(ObjectInfo { + name: k.to_vec(), + creation: *t, + })), + ) + } + } + + impl std::fmt::Debug for MockService { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries( + self.0 + .iter() + .map(|(k, v)| (std::str::from_utf8(k).unwrap(), v)), + ) + .finish() + } + } + + // Add some testing utilities to CloudServer. + impl CloudServer { + fn mock_add_version(&mut self, parent: VersionId, child: VersionId, creation: u64, data: &[u8]) { + let name = Self::version_name(&parent, &child); + let sealed = self + .cryptor + .seal(Unsealed { + version_id: child, + payload: data.into(), + }) + .unwrap(); + self.service.0.insert(name, (creation, sealed.into())); + } + + fn mock_add_snapshot(&mut self, version: VersionId, creation: u64, snapshot: &[u8]) { + let name = Self::snapshot_name(&version); + let sealed = self + .cryptor + .seal(Unsealed { + version_id: version, + payload: snapshot.into(), + }) + .unwrap(); + self.service.0.insert(name, (creation, sealed.into())); + } + + fn mock_set_latest(&mut self, latest: VersionId) { + let latest = version_to_bytes(latest); + self.service.0.insert(LATEST.to_vec(), (INSERTION_TIME, latest)); + } + + /// Create a copy of this server without any data; used for creating a MockService + /// to compare to with `assert_eq!` + fn empty_copy(&self) -> Self { + Self { + cryptor: self.cryptor.clone(), + cleanup_probability: 0, + service: MockService::default(), + add_version_intercept: None, + } + } + + /// Get a decrypted, string-y copy of the data in the HashMap. + fn unencrypted(&self) -> HashMap { + self.service + .0 + .iter() + .map(|(k, v)| { + let kstr = String::from_utf8(k.to_vec()).unwrap(); + if kstr == "latest" { + return (kstr, (v.0, String::from_utf8(v.1.to_vec()).unwrap())); + } + + let version_id; + if let Some((_, v)) = Self::parse_version_name(k) { + version_id = v; + } else if let Some(v) = Self::parse_snapshot_name(k) { + version_id = v; + } else { + return (kstr, (v.0, format!("{:?}", v.1))); + } + + let unsealed = self + .cryptor + .unseal(Sealed { + version_id, + payload: v.1.to_vec(), + }) + .unwrap(); + let vstr = String::from_utf8(unsealed.into()).unwrap(); + (kstr, (v.0, vstr)) + }) + .collect() + } + } + + const SECRET: &[u8] = b"testing"; + + fn make_server() -> CloudServer { + let mut server = CloudServer::new(MockService::default(), SECRET.into()).unwrap(); + // Prevent cleanup during tests. + server.cleanup_probability = 0; + server + } + + #[test] + fn version_name() { + let p = Uuid::parse_str("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8").unwrap(); + let c = Uuid::parse_str("adcf4e350fa54e4aaf9d3f20f3ba5a32").unwrap(); + assert_eq!( + CloudServer::::version_name(&p, &c), + b"v-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8-adcf4e350fa54e4aaf9d3f20f3ba5a32" + ); + } + + #[test] + fn version_name_round_trip() { + let p = Uuid::new_v4(); + let c = Uuid::new_v4(); + assert_eq!( + CloudServer::::parse_version_name( + &CloudServer::::version_name(&p, &c) + ), + Some((p, c)) + ); + } + + #[test] + fn parse_version_name_bad_prefix() { + assert_eq!( + CloudServer::::parse_version_name( + b"X-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8-adcf4e350fa54e4aaf9d3f20f3ba5a32" + ), + None + ); + } + + #[test] + fn parse_version_name_bad_separator() { + assert_eq!( + CloudServer::::parse_version_name( + b"v-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8xadcf4e350fa54e4aaf9d3f20f3ba5a32" + ), + None + ); + } + + #[test] + fn parse_version_name_too_short() { + assert_eq!( + CloudServer::::parse_version_name( + b"v-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8-adcf4e350fa54e4aaf9d3f20f3ba5a3" + ), + None + ); + } + + #[test] + fn parse_version_name_too_long() { + assert_eq!( + CloudServer::::parse_version_name( + b"v-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8-adcf4e350fa54e4aaf9d3f20f3ba5a320" + ), + None + ); + } + + #[test] + fn snapshot_name_round_trip() { + let v = Uuid::new_v4(); + assert_eq!( + CloudServer::::parse_snapshot_name( + &CloudServer::::snapshot_name(&v) + ), + Some(v) + ); + } + + #[test] + fn parse_snapshot_name_invalid() { + assert_eq!( + CloudServer::::parse_snapshot_name(b"s-ggggggggggggggggggd2d3d4d5d6d7d8"), + None + ); + } + + #[test] + fn parse_snapshot_name_bad_prefix() { + assert_eq!( + CloudServer::::parse_snapshot_name(b"s:a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), + None + ); + } + + #[test] + fn parse_snapshot_name_too_short() { + assert_eq!( + CloudServer::::parse_snapshot_name(b"s-a1a2a3a4b1b2c1c2d1d2d3d4d5d6"), + None + ); + } + + #[test] + fn parse_snapshot_name_too_long() { + assert_eq!( + CloudServer::::parse_snapshot_name( + b"s-a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8000" + ), + None + ); + } + + #[test] + fn get_latest_empty() { + let mut server = make_server(); + assert_eq!(server.get_latest().unwrap(), None); + } + + #[test] + fn get_latest_exists() { + let mut server = make_server(); + let latest = Uuid::new_v4(); + server.mock_set_latest(latest); + assert_eq!(server.get_latest().unwrap(), Some(latest)); + } + + #[test] + fn get_latest_invalid() { + let mut server = make_server(); + server + .service + .0 + .insert(LATEST.to_vec(), (999, b"not-a-uuid".to_vec())); + assert!(server.get_latest().is_err()); + } + + #[test] + fn get_child_versions_empty() { + let mut server = make_server(); + assert_eq!(server.get_child_versions(&Uuid::new_v4()).unwrap(), vec![]); + } + + #[test] + fn get_child_versions_single() { + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v2, v1, 1000, b"first"); + assert_eq!(server.get_child_versions(&v1).unwrap(), vec![]); + assert_eq!(server.get_child_versions(&v2).unwrap(), vec![v1]); + } + + #[test] + fn get_child_versions_multiple() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v3, v1, 1000, b"first"); + server.mock_add_version(v3, v2, 1000, b"second"); + assert_eq!(server.get_child_versions(&v1).unwrap(), vec![]); + assert_eq!(server.get_child_versions(&v2).unwrap(), vec![]); + let versions = server.get_child_versions(&v3).unwrap(); + assert!(versions == vec![v1, v2] || versions == vec![v2, v1]); + } + + #[test] + fn add_version_empty() { + let mut server = make_server(); + let parent = Uuid::new_v4(); + let (res, _) = server.add_version(parent, b"history".into()).unwrap(); + assert!(matches!(res, AddVersionResult::Ok(_))); + } + + #[test] + fn add_version_good() { + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"first"); + server.mock_set_latest(v2); + let (res, _) = server.add_version(v2, b"history".into()).unwrap(); + let AddVersionResult::Ok(new_version) = res else { + panic!("expected OK"); + }; + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"first"); + expected.mock_add_version(v2, new_version, INSERTION_TIME, b"history"); + expected.mock_set_latest(new_version); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn add_version_not_latest() { + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"first"); + server.mock_set_latest(v2); + let (res, _) = server.add_version(v1, b"history".into()).unwrap(); + assert_eq!(res, AddVersionResult::ExpectedParentVersion(v2)); + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"first"); + expected.mock_set_latest(v2); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn add_version_not_latest_race() { + // The `add_version` function effectively checks twice for a conflict: once by just + // fetching "latest", returning early if the value is not as expected; and once in the + // compare-and-swap. This test uses `add_version_intercept` to force the first check to + // succeed and the second test to fail. + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + const V3: Uuid = Uuid::max(); + server.mock_add_version(v1, v2, 1000, b"first"); + server.mock_add_version(v2, V3, 1000, b"second"); + server.mock_set_latest(v2); + server.add_version_intercept = Some(|service| { + service.put(&LATEST, &version_to_bytes(V3)).unwrap(); + }); + let (res, _) = server.add_version(v2, b"history".into()).unwrap(); + assert_eq!(res, AddVersionResult::ExpectedParentVersion(V3)); + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"first"); + expected.mock_add_version(v2, V3, 1000, b"second"); + expected.mock_set_latest(V3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn add_version_unknown() { + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"first"); + server.mock_set_latest(v2); + let (res, _) = server + .add_version(Uuid::new_v4(), b"history".into()) + .unwrap(); + assert_eq!(res, AddVersionResult::ExpectedParentVersion(v2)); + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"first"); + expected.mock_set_latest(v2); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn get_child_version_empty() { + let mut server = make_server(); + assert_eq!( + server.get_child_version(Uuid::new_v4()).unwrap(), + GetVersionResult::NoSuchVersion + ); + } + + #[test] + fn get_child_version_single() { + let mut server = make_server(); + let (v1, v2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v2, v1, 1000, b"first"); + assert_eq!( + server.get_child_version(v1).unwrap(), + GetVersionResult::NoSuchVersion + ); + assert_eq!( + server.get_child_version(v2).unwrap(), + GetVersionResult::Version { + version_id: v1, + parent_version_id: v2, + history_segment: b"first".to_vec(), + } + ); + } + + #[test] + fn get_child_version_multiple() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let (vx, vy, vz) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v1, vx, 1000, b"false start x"); + server.mock_add_version(v1, vy, 1000, b"false start y"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_add_version(v2, vz, 1000, b"false start z"); + server.mock_set_latest(v3); + assert_eq!( + server.get_child_version(v1).unwrap(), + GetVersionResult::Version { + version_id: v2, + parent_version_id: v1, + history_segment: b"second".to_vec(), + } + ); + assert_eq!( + server.get_child_version(v2).unwrap(), + GetVersionResult::Version { + version_id: v3, + parent_version_id: v2, + history_segment: b"third".to_vec(), + } + ); + assert_eq!( + server.get_child_version(v3).unwrap(), + GetVersionResult::NoSuchVersion + ); + } + + #[test] + fn cleanup_empty() { + let mut server = make_server(); + server.cleanup().unwrap(); + } + + #[test] + fn cleanup_linear() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(NIL_VERSION_ID, v1, 1000, b"first"); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_add_snapshot(v1, 1000, b"snap 1"); + server.mock_set_latest(v3); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_version(NIL_VERSION_ID, v1, 1000, b"first"); + expected.mock_add_version(v1, v2, 1000, b"second"); + expected.mock_add_version(v2, v3, 1000, b"third"); + expected.mock_add_snapshot(v1, 1000, b"snap 1"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_cycle() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v3, v1, 1000, b"first"); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_set_latest(v3); + + assert!(server.cleanup().is_err()); + + let mut expected = server.empty_copy(); + expected.mock_add_version(v3, v1, 1000, b"first"); + expected.mock_add_version(v1, v2, 1000, b"second"); + expected.mock_add_version(v2, v3, 1000, b"third"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_extra_branches() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let (vx, vy) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v1, vx, 1000, b"false start x"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_add_version(v2, vy, 1000, b"false start y"); + server.mock_set_latest(v3); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"second"); + expected.mock_add_version(v2, v3, 1000, b"third"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_extra_snapshots() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let vy = Uuid::new_v4(); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_add_version(v2, vy, 1000, b"false start y"); + server.mock_add_snapshot(v1, 1000, b"snap 1"); + server.mock_add_snapshot(v2, 1000, b"snap 2"); + server.mock_add_snapshot(vy, 1000, b"snap y"); + server.mock_set_latest(v3); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"second"); + expected.mock_add_version(v2, v3, 1000, b"third"); + expected.mock_add_snapshot(v2, 1000, b"snap 2"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_old_versions_no_snapshot() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 200, b"second"); + server.mock_add_version(v2, v3, 300, b"third"); + server.mock_set_latest(v3); + + server.cleanup().unwrap(); + + // Nothing is deleted. + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 200, b"second"); + expected.mock_add_version(v2, v3, 300, b"third"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_old_versions_with_snapshot() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let (v4, v5, v6) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 200, b"second"); + server.mock_add_version(v2, v3, 300, b"third"); + server.mock_add_version(v3, v4, 1400, b"fourth"); + server.mock_add_version(v4, v5, 1500, b"fifth"); + server.mock_add_snapshot(v5, 1501, b"snap 1"); + server.mock_add_version(v5, v6, 1600, b"sixth"); + server.mock_set_latest(v6); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_version(v3, v4, 1400, b"fourth"); // Not old enough to be deleted. + expected.mock_add_version(v4, v5, 1500, b"fifth"); + expected.mock_add_snapshot(v5, 1501, b"snap 1"); + expected.mock_add_version(v5, v6, 1600, b"sixth"); + expected.mock_set_latest(v6); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_old_versions_newer_than_snapshot() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let (v4, v5, v6) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 200, b"second"); + server.mock_add_version(v2, v3, 300, b"third"); + server.mock_add_snapshot(v3, 301, b"snap 1"); + server.mock_add_version(v3, v4, 400, b"fourth"); + server.mock_add_version(v4, v5, 500, b"fifth"); + server.mock_add_version(v5, v6, 600, b"sixth"); + server.mock_set_latest(v6); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_snapshot(v3, 301, b"snap 1"); + expected.mock_add_version(v3, v4, 400, b"fourth"); + expected.mock_add_version(v4, v5, 500, b"fifth"); + expected.mock_add_version(v5, v6, 600, b"sixth"); + expected.mock_set_latest(v6); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn cleanup_children_of_latest() { + let mut server = make_server(); + let (v1, v2, v3) = (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()); + let (vnew1, vnew2) = (Uuid::new_v4(), Uuid::new_v4()); + server.mock_add_version(v1, v2, 1000, b"second"); + server.mock_add_version(v2, v3, 1000, b"third"); + server.mock_add_version(v3, vnew1, 1000, b"new 1"); + server.mock_add_version(v3, vnew2, 1000, b"new 2"); + // Two replicas are adding new versions, but v3 is still latest. + server.mock_set_latest(v3); + + server.cleanup().unwrap(); + + let mut expected = server.empty_copy(); + expected.mock_add_version(v1, v2, 1000, b"second"); + expected.mock_add_version(v2, v3, 1000, b"third"); + // New versions that are children of latest are not deleted. + expected.mock_add_version(v3, vnew1, 1000, b"new 1"); + expected.mock_add_version(v3, vnew2, 1000, b"new 2"); + expected.mock_set_latest(v3); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn add_snapshot() { + let mut server = make_server(); + let v = Uuid::new_v4(); + server.add_snapshot(v, b"SNAP".into()).unwrap(); + let mut expected = server.empty_copy(); + expected.mock_add_snapshot(v, INSERTION_TIME, b"SNAP"); + assert_eq!(server.unencrypted(), expected.unencrypted()); + } + + #[test] + fn get_snapshot_missing() { + let mut server = make_server(); + assert_eq!(server.get_snapshot().unwrap(), None); + } + + #[test] + fn get_snapshot_present() { + let mut server = make_server(); + let v = Uuid::new_v4(); + server.mock_add_snapshot(v, 1000, b"SNAP"); + assert_eq!(server.get_snapshot().unwrap(), Some((v, b"SNAP".into()))); + } +} diff --git a/taskchampion/taskchampion/src/server/cloud/service.rs b/taskchampion/taskchampion/src/server/cloud/service.rs new file mode 100644 index 000000000..84b1c24ef --- /dev/null +++ b/taskchampion/taskchampion/src/server/cloud/service.rs @@ -0,0 +1,38 @@ +use crate::errors::Result; + +/// Information about an object as returned from `Service::list` +pub(in crate::server) struct ObjectInfo { + /// Name of the object. + pub(in crate::server) name: Vec, + /// Creation time of the object, in seconds since the UNIX epoch. + pub(in crate::server) creation: u64, +} + +/// An abstraction of a cloud-storage service. +/// +/// The underlying cloud storage is assumed to be a map from object names to object values, +/// similar to a HashMap, with the addition of a compare-and-swap operation. Object names +/// are always simple strings from the character set `[a-zA-Z0-9-]`, no more than 100 characters +/// in length. +pub(in crate::server) trait Service { + /// Put an object into cloud storage. If the object exists, it is overwritten. + fn put(&mut self, name: &[u8], value: &[u8]) -> Result<()>; + + /// Get an object from cloud storage, or None if the object does not exist. + fn get(&mut self, name: &[u8]) -> Result>>; + + /// Delete an object. Does nothing if the object does not exist. + fn del(&mut self, name: &[u8]) -> Result<()>; + + /// Enumerate objects with the given prefix. + fn list<'a>(&'a mut self, prefix: &[u8]) -> Box> + 'a>; + + /// Compare the existing object's value with `existing_value`, and replace with `new_value` + /// only if the values match. Returns true if the replacement occurred. + fn compare_and_swap( + &mut self, + name: &[u8], + existing_value: Option>, + new_value: Vec, + ) -> Result; +} diff --git a/taskchampion/taskchampion/src/server/config.rs b/taskchampion/taskchampion/src/server/config.rs index 0d2b6ab07..9b9f5d698 100644 --- a/taskchampion/taskchampion/src/server/config.rs +++ b/taskchampion/taskchampion/src/server/config.rs @@ -1,5 +1,9 @@ use super::types::Server; use crate::errors::Result; +#[cfg(feature = "server-gcp")] +use crate::server::cloud::gcp::GcpService; +#[cfg(feature = "cloud")] +use crate::server::cloud::CloudServer; use crate::server::local::LocalServer; #[cfg(feature = "server-sync")] use crate::server::sync::SyncServer; @@ -23,6 +27,17 @@ pub enum ServerConfig { /// Client ID to identify and authenticate this replica to the server client_id: Uuid, + /// Private encryption secret used to encrypt all data sent to the server. This can + /// be any suitably un-guessable string of bytes. + encryption_secret: Vec, + }, + /// A remote taskchampion-sync-server instance + #[cfg(feature = "server-gcp")] + Gcp { + /// Bucket in which to store the task data. This bucket must not be used for any other + /// purpose. + bucket: String, + /// Private encryption secret used to encrypt all data sent to the server. This can /// be any suitably un-guessable string of bytes. encryption_secret: Vec, @@ -40,6 +55,14 @@ impl ServerConfig { client_id, encryption_secret, } => Box::new(SyncServer::new(origin, client_id, encryption_secret)?), + #[cfg(feature = "server-gcp")] + ServerConfig::Gcp { + bucket, + encryption_secret, + } => Box::new(CloudServer::new( + GcpService::new(bucket)?, + encryption_secret, + )?), }) } } diff --git a/taskchampion/taskchampion/src/server/crypto.rs b/taskchampion/taskchampion/src/server/crypto.rs index 9dfe78ede..004d03070 100644 --- a/taskchampion/taskchampion/src/server/crypto.rs +++ b/taskchampion/taskchampion/src/server/crypto.rs @@ -11,23 +11,24 @@ const TASK_APP_ID: u8 = 1; /// An Cryptor stores a secret and allows sealing and unsealing. It derives a key from the secret, /// which takes a nontrivial amount of time, so it should be created once and re-used for the given -/// client_id. +/// context. +#[derive(Clone)] pub(super) struct Cryptor { key: aead::LessSafeKey, rng: rand::SystemRandom, } impl Cryptor { - pub(super) fn new(client_id: Uuid, secret: &Secret) -> Result { + pub(super) fn new(salt: impl AsRef<[u8]>, secret: &Secret) -> Result { Ok(Cryptor { - key: Self::derive_key(client_id, secret)?, + key: Self::derive_key(salt, secret)?, rng: rand::SystemRandom::new(), }) } /// Derive a key as specified for version 1. Note that this may take 10s of ms. - fn derive_key(client_id: Uuid, secret: &Secret) -> Result { - let salt = digest::digest(&digest::SHA256, client_id.as_bytes()); + fn derive_key(salt: impl AsRef<[u8]>, secret: &Secret) -> Result { + let salt = digest::digest(&digest::SHA256, salt.as_ref()); let mut key_bytes = vec![0u8; aead::CHACHA20_POLY1305.key_len()]; pbkdf2::derive( @@ -169,42 +170,30 @@ pub(super) struct Unsealed { pub(super) payload: Vec, } +impl Into> for Unsealed { + fn into(self) -> Vec { + self.payload + } +} + /// An encrypted payload pub(super) struct Sealed { pub(super) version_id: Uuid, pub(super) payload: Vec, } -impl Sealed { - #[cfg(feature = "server-sync")] - pub(super) fn from_resp( - resp: ureq::Response, - version_id: Uuid, - content_type: &str, - ) -> Result { - use std::io::Read; - if resp.header("Content-Type") == Some(content_type) { - let mut reader = resp.into_reader(); - let mut payload = vec![]; - reader.read_to_end(&mut payload)?; - Ok(Self { - version_id, - payload, - }) - } else { - Err(Error::Server(String::from( - "Response did not have expected content-type", - ))) - } - } -} - impl AsRef<[u8]> for Sealed { fn as_ref(&self) -> &[u8] { self.payload.as_ref() } } +impl Into> for Sealed { + fn into(self) -> Vec { + self.payload + } +} + #[cfg(test)] mod test { use super::*; @@ -269,10 +258,10 @@ mod test { fn round_trip_bad_key() { let version_id = Uuid::new_v4(); let payload = b"HISTORY REPEATS ITSELF".to_vec(); - let client_id = Uuid::new_v4(); + let salt = Uuid::new_v4(); let secret = Secret(b"SEKRIT".to_vec()); - let cryptor = Cryptor::new(client_id, &secret).unwrap(); + let cryptor = Cryptor::new(salt, &secret).unwrap(); let unsealed = Unsealed { version_id, @@ -281,7 +270,7 @@ mod test { let sealed = cryptor.seal(unsealed).unwrap(); let secret = Secret(b"DIFFERENT_SECRET".to_vec()); - let cryptor = Cryptor::new(client_id, &secret).unwrap(); + let cryptor = Cryptor::new(salt, &secret).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } @@ -289,10 +278,10 @@ mod test { fn round_trip_bad_version() { let version_id = Uuid::new_v4(); let payload = b"HISTORY REPEATS ITSELF".to_vec(); - let client_id = Uuid::new_v4(); + let salt = Uuid::new_v4(); let secret = Secret(b"SEKRIT".to_vec()); - let cryptor = Cryptor::new(client_id, &secret).unwrap(); + let cryptor = Cryptor::new(salt, &secret).unwrap(); let unsealed = Unsealed { version_id, @@ -304,13 +293,13 @@ mod test { } #[test] - fn round_trip_bad_client_id() { + fn round_trip_bad_salt() { let version_id = Uuid::new_v4(); let payload = b"HISTORY REPEATS ITSELF".to_vec(); - let client_id = Uuid::new_v4(); + let salt = Uuid::new_v4(); let secret = Secret(b"SEKRIT".to_vec()); - let cryptor = Cryptor::new(client_id, &secret).unwrap(); + let cryptor = Cryptor::new(salt, &secret).unwrap(); let unsealed = Unsealed { version_id, @@ -318,8 +307,8 @@ mod test { }; let sealed = cryptor.seal(unsealed).unwrap(); - let client_id = Uuid::new_v4(); - let cryptor = Cryptor::new(client_id, &secret).unwrap(); + let salt = Uuid::new_v4(); + let cryptor = Cryptor::new(salt, &secret).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } @@ -341,13 +330,13 @@ mod test { #[test] fn good() { - let (version_id, client_id, encryption_secret) = defaults(); + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-good.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); let unsealed = cryptor.unseal(sealed).unwrap(); assert_eq!(unsealed.payload, b"SUCCESS"); @@ -356,61 +345,61 @@ mod test { #[test] fn bad_version_id() { - let (version_id, client_id, encryption_secret) = defaults(); + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-bad-version-id.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } #[test] - fn bad_client_id() { - let (version_id, client_id, encryption_secret) = defaults(); + fn bad_salt() { + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-bad-client-id.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } #[test] fn bad_secret() { - let (version_id, client_id, encryption_secret) = defaults(); + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-bad-secret.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } #[test] fn bad_version() { - let (version_id, client_id, encryption_secret) = defaults(); + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-bad-version.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } #[test] fn bad_app_id() { - let (version_id, client_id, encryption_secret) = defaults(); + let (version_id, salt, encryption_secret) = defaults(); let sealed = Sealed { version_id, payload: include_bytes!("test-bad-app-id.data").to_vec(), }; - let cryptor = Cryptor::new(client_id, &Secret(encryption_secret)).unwrap(); + let cryptor = Cryptor::new(salt, &Secret(encryption_secret)).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } } diff --git a/taskchampion/taskchampion/src/server/mod.rs b/taskchampion/taskchampion/src/server/mod.rs index a80675d4d..cafa2d194 100644 --- a/taskchampion/taskchampion/src/server/mod.rs +++ b/taskchampion/taskchampion/src/server/mod.rs @@ -22,6 +22,9 @@ mod crypto; #[cfg(feature = "server-sync")] mod sync; +#[cfg(feature = "cloud")] +mod cloud; + pub use config::ServerConfig; pub use types::*; diff --git a/taskchampion/taskchampion/src/server/sync/mod.rs b/taskchampion/taskchampion/src/server/sync/mod.rs index dd92e615b..b38b2ce2e 100644 --- a/taskchampion/taskchampion/src/server/sync/mod.rs +++ b/taskchampion/taskchampion/src/server/sync/mod.rs @@ -1,4 +1,4 @@ -use crate::errors::Result; +use crate::errors::{Error, Result}; use crate::server::{ AddVersionResult, GetVersionResult, HistorySegment, Server, Snapshot, SnapshotUrgency, VersionId, @@ -62,6 +62,23 @@ fn get_snapshot_urgency(resp: &ureq::Response) -> SnapshotUrgency { } } +fn sealed_from_resp(resp: ureq::Response, version_id: Uuid, content_type: &str) -> Result { + use std::io::Read; + if resp.header("Content-Type") == Some(content_type) { + let mut reader = resp.into_reader(); + let mut payload = vec![]; + reader.read_to_end(&mut payload)?; + Ok(Sealed { + version_id, + payload, + }) + } else { + Err(Error::Server(String::from( + "Response did not have expected content-type", + ))) + } +} + impl Server for SyncServer { fn add_version( &mut self, @@ -117,7 +134,7 @@ impl Server for SyncServer { let parent_version_id = get_uuid_header(&resp, "X-Parent-Version-Id")?; let version_id = get_uuid_header(&resp, "X-Version-Id")?; let sealed = - Sealed::from_resp(resp, parent_version_id, HISTORY_SEGMENT_CONTENT_TYPE)?; + sealed_from_resp(resp, parent_version_id, HISTORY_SEGMENT_CONTENT_TYPE)?; let history_segment = self.cryptor.unseal(sealed)?.payload; Ok(GetVersionResult::Version { version_id, @@ -158,7 +175,7 @@ impl Server for SyncServer { { Ok(resp) => { let version_id = get_uuid_header(&resp, "X-Version-Id")?; - let sealed = Sealed::from_resp(resp, version_id, SNAPSHOT_CONTENT_TYPE)?; + let sealed = sealed_from_resp(resp, version_id, SNAPSHOT_CONTENT_TYPE)?; let snapshot = self.cryptor.unseal(sealed)?.payload; Ok(Some((version_id, snapshot))) }