From fefa88fc2a10c66958341b98ef49d6d40f9eada8 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:55:59 -0700 Subject: [PATCH] Feature/cli clearnet (#2789) * add support for ACME cert acquisition * add support for modifying hosts for a package * misc fixes * more fixes * use different port for lan clearnet than wan clearnet * fix chroot-and-upgrade always growing * bail on failure * wip * fix alpn auth * bump async-acme * fix cli * add barebones documentation * add domain to hostname info --- CLEARNET.md | 40 + build/lib/scripts/chroot-and-upgrade | 8 +- build/lib/scripts/prune-images | 3 +- core/Cargo.lock | 821 ++++++++++++++---- core/models/src/errors.rs | 5 + core/startos/Cargo.toml | 4 + core/startos/src/db/model/public.rs | 16 + core/startos/src/lib.rs | 11 +- core/startos/src/net/acme.rs | 324 +++++++ core/startos/src/net/host/address.rs | 9 + core/startos/src/net/host/mod.rs | 163 ++++ core/startos/src/net/keys.rs | 5 +- core/startos/src/net/mod.rs | 5 + core/startos/src/net/net_controller.rs | 41 +- core/startos/src/net/service_interface.rs | 9 +- core/startos/src/net/tor.rs | 84 +- core/startos/src/net/vhost.rs | 186 +++- core/startos/src/service/mod.rs | 26 +- core/startos/src/util/serde.rs | 30 + sdk/base/lib/osBindings/AcmeSettings.ts | 13 + sdk/base/lib/osBindings/ServerInfo.ts | 2 + sdk/base/lib/osBindings/index.ts | 1 + .../ui/src/app/services/api/mock-patch.ts | 1 + 23 files changed, 1589 insertions(+), 218 deletions(-) create mode 100644 CLEARNET.md create mode 100644 core/startos/src/net/acme.rs create mode 100644 sdk/base/lib/osBindings/AcmeSettings.ts diff --git a/CLEARNET.md b/CLEARNET.md new file mode 100644 index 000000000..457a2e4f7 --- /dev/null +++ b/CLEARNET.md @@ -0,0 +1,40 @@ +# Setting up clearnet for a service interface + +NOTE: this guide is for HTTPS only! Other configurations may require a more bespoke setup depending on the service. Please consult the service documentation or the Start9 Community for help with non-HTTPS applications + +## Initialize ACME certificate generation + +The following command will register your device with an ACME certificate provider, such as letsencrypt + +This only needs to be done once. + +``` +start-cli net acme init --provider=letsencrypt --contact="mailto:me@drbonez.dev" +``` + +- `provider` can be `letsencrypt`, `letsencrypt-staging` (useful if you're doing a lot of testing and want to avoid being rate limited), or the url of any provider that supports the [RFC8555](https://datatracker.ietf.org/doc/html/rfc8555) ACME api +- `contact` can be any valid contact url, typically `mailto:` urls. it can be specified multiple times to set multiple contacts + +## Whitelist a domain for ACME certificate acquisition + +The following command will tell the OS to use ACME certificates instead of system signed ones for the provided url. In this example, `testing.drbonez.dev` + +This must be done for every domain you wish to host on clearnet. + +``` +start-cli net acme domain add "testing.drbonez.dev" +``` + +## Forward clearnet port + +Go into your router settings, and map port 443 on your router to port 5443 on your start-os device. This one port should cover most use cases + +## Add domain to service host + +The following command will tell the OS to route https requests from the WAN to the provided hostname to the specified service. In this example, we are adding `testing.drbonez.dev` to the host `ui-multi` on the package `hello-world`. To see a list of available host IDs for a given package, run `start-cli package host list` + +This must be done for every domain you wish to host on clearnet. + +``` +start-cli package host hello-world address ui-multi add testing.drbonez.dev +``` diff --git a/build/lib/scripts/chroot-and-upgrade b/build/lib/scripts/chroot-and-upgrade index be2911369..8c3da37b4 100755 --- a/build/lib/scripts/chroot-and-upgrade +++ b/build/lib/scripts/chroot-and-upgrade @@ -77,6 +77,7 @@ umount /media/startos/next/dev umount /media/startos/next/sys umount /media/startos/next/proc umount /media/startos/next/boot +umount /media/startos/next/media/startos/root if [ "$CHROOT_RES" -eq 0 ]; then @@ -86,7 +87,12 @@ if [ "$CHROOT_RES" -eq 0 ]; then echo 'Upgrading...' - time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip + if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then + umount -R /media/startos/next + umount -R /media/startos/upper + rm -rf /media/startos/upper /media/startos/next + exit 1 + fi hash=$(b3sum /media/startos/images/next.squashfs | head -c 32) mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs diff --git a/build/lib/scripts/prune-images b/build/lib/scripts/prune-images index 20356a28c..1203d2377 100755 --- a/build/lib/scripts/prune-images +++ b/build/lib/scripts/prune-images @@ -33,10 +33,11 @@ if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/cur echo 'Pruning...' current="$(readlink -f /media/startos/config/current.rootfs)" while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do - to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs | grep -v "$current" | tail -n1)" + to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)" if [ -e "$to_prune" ]; then echo " Pruning $to_prune" rm -rf "$to_prune" + sync else >&2 echo "Not enough space and nothing to prune!" exit 1 diff --git a/core/Cargo.lock b/core/Cargo.lock index 490edf223..498e5903e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "android-tzdata" @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arrayref" @@ -193,6 +193,67 @@ dependencies = [ "term", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-acme" +version = "0.5.0" +source = "git+https://github.com/dr-bonez/async-acme.git#b9ff31ad900adc9086c0d1437ce51661d30856d2" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures-util", + "generic-async-http-client", + "log", + "pem", + "rcgen", + "ring", + "rustls 0.23.17", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "thiserror", + "tokio", + "x509-parser", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -237,7 +298,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -248,7 +309,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -274,9 +335,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +checksum = "fe7c2840b66236045acd2607d5866e274380afd87ef99d6226e961e2cb47df45" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -286,9 +347,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" dependencies = [ "bindgen", "cc", @@ -329,9 +390,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core 0.4.5", @@ -341,7 +402,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "itoa", "matchit", @@ -413,7 +474,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "pin-project-lite", "tokio", @@ -538,7 +599,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.86", + "syn 2.0.87", "which", ] @@ -675,9 +736,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -705,9 +766,9 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.1.31" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -818,9 +879,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -828,9 +889,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -847,14 +908,14 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "cmake" @@ -925,7 +986,7 @@ dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.12", "windows-sys 0.52.0", ] @@ -1027,12 +1088,13 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ "cookie", - "idna 0.5.0", + "document-features", + "idna 1.0.3", "log", "publicsuffix", "serde", @@ -1060,9 +1122,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ "libc", ] @@ -1190,9 +1252,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -1255,7 +1317,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1279,7 +1341,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1290,7 +1352,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1321,7 +1383,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1336,6 +1398,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.7.3" @@ -1344,7 +1420,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1367,7 +1443,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1412,12 +1488,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "divrem" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1589,7 +1685,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1676,9 +1772,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fd-lock-rs" @@ -1725,9 +1821,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -1868,7 +1964,18 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", +] + +[[package]] +name = "futures-rustls" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" +dependencies = [ + "futures-io", + "rustls 0.22.4", + "rustls-pki-types", ] [[package]] @@ -1912,6 +2019,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-async-http-client" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cec8bb4d3d32542cfcb9517f78366b52c17931e30d7ee1682c13686c19cee7" +dependencies = [ + "futures", + "futures-rustls", + "hyper 1.5.1", + "log", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.25.0", + "webpki-roots 0.26.6", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1990,9 +2116,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -2050,9 +2176,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "hashlink" @@ -2259,14 +2385,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.6", + "h2 0.4.7", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -2286,12 +2412,12 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", - "rustls 0.23.16", + "rustls 0.23.17", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", ] @@ -2315,7 +2441,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-util", "native-tls", "tokio", @@ -2334,7 +2460,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", @@ -2365,6 +2491,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "id-pool" version = "0.2.2" @@ -2380,16 +2624,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.4.0" @@ -2402,19 +2636,30 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "image" -version = "0.25.4" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -2499,22 +2744,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "serde", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", "tokio", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -2526,15 +2771,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "integer-encoding" version = "4.0.2" @@ -2784,7 +3020,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.8", + "regex-automata 0.4.9", ] [[package]] @@ -2833,9 +3069,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libloading" @@ -2887,6 +3123,18 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -3038,7 +3286,7 @@ checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" name = "models" version = "0.1.0" dependencies = [ - "axum 0.7.7", + "axum 0.7.9", "base64 0.21.7", "color-eyre", "ed25519-dalek 2.1.1", @@ -3302,7 +3550,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3320,6 +3568,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -3368,7 +3625,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3379,9 +3636,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.4.0+3.4.0" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] @@ -3529,6 +3786,16 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3575,7 +3842,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3631,7 +3898,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3707,7 +3974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3721,7 +3988,7 @@ dependencies = [ "is-terminal", "lazy_static", "term", - "unicode-width", + "unicode-width 0.1.12", ] [[package]] @@ -3805,7 +4072,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3828,7 +4095,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3848,11 +4115,11 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "publicsuffix" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" dependencies = [ - "idna 0.3.0", + "idna 1.0.3", "psl-types", ] @@ -3991,6 +4258,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -4034,7 +4313,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.8", + "regex-automata 0.4.9", "regex-syntax 0.8.5", ] @@ -4049,9 +4328,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -4083,11 +4362,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.6", + "h2 0.4.7", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4169,11 +4448,11 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.2.3" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor%2Fno-dyn-ctx#021379f21c4d11c5a62c07460f4531ce9b555155" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor%2Fno-dyn-ctx#21e35d85fb8f5de0e046c7ab5266236c2b639a4b" dependencies = [ "async-stream", "async-trait", - "axum 0.7.7", + "axum 0.7.9", "clap", "futures", "http 1.1.0", @@ -4258,10 +4537,19 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.38.38" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno 0.3.9", @@ -4283,13 +4571,28 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -4373,7 +4676,7 @@ dependencies = [ "thingbuf", "thiserror", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.12", ] [[package]] @@ -4393,9 +4696,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -4445,9 +4748,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -4464,9 +4767,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -4490,20 +4793,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "indexmap 2.6.0", "itoa", @@ -4522,6 +4825,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -4570,7 +4884,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -4832,7 +5146,7 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -5003,8 +5317,8 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.86", - "unicode-width", + "syn 2.0.87", + "unicode-width 0.1.12", ] [[package]] @@ -5049,15 +5363,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "start-os" version = "0.3.6-alpha.8" dependencies = [ "aes", + "async-acme", "async-compression", "async-stream", "async-trait", - "axum 0.7.7", + "axum 0.7.9", "axum-server", "backhand", "barrage", @@ -5159,7 +5480,7 @@ dependencies = [ "textwrap", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-socks", "tokio-stream", "tokio-tar", @@ -5245,9 +5566,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -5269,6 +5590,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -5298,9 +5630,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", @@ -5309,9 +5641,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -5348,7 +5680,7 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.1.12", ] [[package]] @@ -5363,22 +5695,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5442,6 +5774,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -5459,9 +5801,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -5494,7 +5836,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5507,13 +5849,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.16", + "rustls 0.23.17", "rustls-pki-types", "tokio", ] @@ -5768,7 +6121,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5920,7 +6273,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "termcolor", ] @@ -5990,7 +6343,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -6062,6 +6415,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -6092,12 +6451,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -6114,6 +6473,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -6215,7 +6586,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -6249,7 +6620,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6283,12 +6654,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -6547,6 +6937,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.2.0" @@ -6562,6 +6964,23 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "xattr" version = "0.2.3" @@ -6615,6 +7034,39 @@ dependencies = [ "serde", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -6633,7 +7085,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", ] [[package]] @@ -6653,7 +7126,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 708b57861..2077a8bbd 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -322,6 +322,11 @@ impl From for Error { Error::new(e, kind) } } +impl From for Error { + fn from(e: torut::onion::OnionAddressParseError) -> Self { + Error::new(e, ErrorKind::Tor) + } +} impl From for Error { fn from(value: patch_db::value::Error) -> Self { match value.kind { diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index df4ac2378..e2a20a0a1 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -50,6 +50,10 @@ test = [] [dependencies] aes = { version = "0.7.5", features = ["ctr"] } +async-acme = { version = "0.5.0", git = "https://github.com/dr-bonez/async-acme.git", features = [ + "use_rustls", + "use_tokio", +] } async-compression = { version = "0.4.4", features = [ "gzip", "brotli", diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 353c1f89b..85978134d 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -55,6 +55,7 @@ impl Public { .parse() .unwrap(), ip_info: BTreeMap::new(), + acme: None, status_info: ServerStatus { backup_progress: None, updated: false, @@ -130,6 +131,7 @@ pub struct ServerInfo { #[ts(type = "string")] pub tor_address: Url, pub ip_info: BTreeMap, + pub acme: Option, #[serde(default)] pub status_info: ServerStatus, pub wifi: WifiInfo, @@ -174,6 +176,20 @@ impl IpInfo { } } +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct AcmeSettings { + #[ts(type = "string")] + pub provider: Url, + /// email addresses for letsencrypt + pub contact: Vec, + #[ts(type = "string[]")] + /// domains to get letsencrypt certs for + pub domains: BTreeSet, +} + #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[model = "Model"] #[ts(export)] diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 83c4d462d..3c5875e36 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -355,7 +355,7 @@ pub fn package() -> ParentHandler { from_fn_async(control::start) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Start a package container") + .with_about("Start a service") .with_call_remote::(), ) .subcommand( @@ -363,7 +363,7 @@ pub fn package() -> ParentHandler { from_fn_async(control::stop) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Stop a package container") + .with_about("Stop a service") .with_call_remote::(), ) .subcommand( @@ -371,7 +371,7 @@ pub fn package() -> ParentHandler { from_fn_async(control::restart) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Restart a package container") + .with_about("Restart a service") .with_call_remote::(), ) .subcommand( @@ -409,9 +409,14 @@ pub fn package() -> ParentHandler { "attach", from_fn_async(service::attach) .with_metadata("get_session", Value::Bool(true)) + .with_about("Execute commands within a service container") .no_cli(), ) .subcommand("attach", from_fn_async(service::cli_attach).no_display()) + .subcommand( + "host", + net::host::host::().with_about("Manage network hosts for a package"), + ) } pub fn diagnostic_api() -> ParentHandler { diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs new file mode 100644 index 000000000..95f9d4adb --- /dev/null +++ b/core/startos/src/net/acme.rs @@ -0,0 +1,324 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use clap::Parser; +use imbl_value::InternedString; +use itertools::Itertools; +use models::{ErrorData, FromStrParser}; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::AcmeSettings; +use crate::db::model::Database; +use crate::prelude::*; +use crate::util::serde::{Pem, Pkcs8Doc}; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct AcmeCertStore { + pub accounts: BTreeMap>, Pem>, + pub certs: BTreeMap>, AcmeCert>>, +} +impl AcmeCertStore { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct AcmeCert { + pub key: Pem>, + pub fullchain: Vec>, +} + +pub struct AcmeCertCache<'a>(pub &'a TypedPatchDb); +#[async_trait::async_trait] +impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> { + type Error = ErrorData; + + async fn read_account(&self, contacts: &[&str]) -> Result>, Self::Error> { + let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec()); + let Some(account) = self + .0 + .peek() + .await + .into_private() + .into_key_store() + .into_acme() + .into_accounts() + .into_idx(&contacts) + else { + return Ok(None); + }; + Ok(Some(account.de()?.0.document.into_vec())) + } + + async fn write_account(&self, contacts: &[&str], contents: &[u8]) -> Result<(), Self::Error> { + let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec()); + let key = Pkcs8Doc { + tag: "EC PRIVATE KEY".into(), + document: pkcs8::Document::try_from(contents).with_kind(ErrorKind::Pem)?, + }; + self.0 + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_acme_mut() + .as_accounts_mut() + .insert(&contacts, &Pem::new(key)) + }) + .await?; + Ok(()) + } + + async fn read_certificate( + &self, + domains: &[String], + directory_url: &str, + ) -> Result, Self::Error> { + let domains = JsonKey::new(domains.into_iter().map(InternedString::intern).collect()); + let directory_url = directory_url + .parse::() + .with_kind(ErrorKind::ParseUrl)?; + let Some(cert) = self + .0 + .peek() + .await + .into_private() + .into_key_store() + .into_acme() + .into_certs() + .into_idx(&directory_url) + .and_then(|a| a.into_idx(&domains)) + else { + return Ok(None); + }; + let cert = cert.de()?; + Ok(Some(( + String::from_utf8( + cert.key + .0 + .private_key_to_pem_pkcs8() + .with_kind(ErrorKind::OpenSsl)?, + ) + .with_kind(ErrorKind::Utf8)?, + cert.fullchain + .into_iter() + .map(|cert| { + String::from_utf8(cert.0.to_pem().with_kind(ErrorKind::OpenSsl)?) + .with_kind(ErrorKind::Utf8) + }) + .collect::, _>>()? + .join("\n"), + ))) + } + + async fn write_certificate( + &self, + domains: &[String], + directory_url: &str, + key_pem: &str, + certificate_pem: &str, + ) -> Result<(), Self::Error> { + tracing::info!("Saving new certificate for {domains:?}"); + let domains = JsonKey::new(domains.into_iter().map(InternedString::intern).collect()); + let directory_url = directory_url + .parse::() + .with_kind(ErrorKind::ParseUrl)?; + let cert = AcmeCert { + key: Pem(PKey::::private_key_from_pem(key_pem.as_bytes()) + .with_kind(ErrorKind::OpenSsl)?), + fullchain: X509::stack_from_pem(certificate_pem.as_bytes()) + .with_kind(ErrorKind::OpenSsl)? + .into_iter() + .map(Pem) + .collect(), + }; + self.0 + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_acme_mut() + .as_certs_mut() + .upsert(&directory_url, || Ok(BTreeMap::new()))? + .insert(&domains, &cert) + }) + .await?; + + Ok(()) + } +} + +pub fn acme() -> ParentHandler { + ParentHandler::new() + .subcommand( + "init", + from_fn_async(init) + .no_display() + .with_about("Setup ACME certificate acquisition") + .with_call_remote::(), + ) + .subcommand( + "domain", + domain::() + .with_about("Add, remove, or view domains for which to acquire ACME certificates"), + ) +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct AcmeProvider(pub Url); +impl FromStr for AcmeProvider { + type Err = ::Err; + fn from_str(s: &str) -> Result { + match s { + "letsencrypt" => async_acme::acme::LETS_ENCRYPT_PRODUCTION_DIRECTORY.parse(), + "letsencrypt-staging" => async_acme::acme::LETS_ENCRYPT_STAGING_DIRECTORY.parse(), + s => s.parse(), + } + .map(Self) + } +} +impl ValueParserFactory for AcmeProvider { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct InitAcmeParams { + #[arg(long)] + pub provider: AcmeProvider, + #[arg(long)] + pub contact: Vec, +} + +pub async fn init( + ctx: RpcContext, + InitAcmeParams { + provider: AcmeProvider(provider), + contact, + }: InitAcmeParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_acme_mut() + .map_mutate(|acme| { + Ok(Some(AcmeSettings { + provider, + contact, + domains: acme.map(|acme| acme.domains).unwrap_or_default(), + })) + }) + }) + .await?; + Ok(()) +} + +pub fn domain() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_domain) + .no_display() + .with_about("Add a domain for which to acquire ACME certificates") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_domain) + .no_display() + .with_about("Remove a domain for which to acquire ACME certificates") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_domains) + .with_custom_display_fn(|_, res| { + for domain in res { + println!("{domain}") + } + Ok(()) + }) + .with_about("List domains for which to acquire ACME certificates") + .with_call_remote::(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct DomainParams { + pub domain: InternedString, +} + +pub async fn add_domain( + ctx: RpcContext, + DomainParams { domain }: DomainParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_acme_mut() + .transpose_mut() + .ok_or_else(|| { + Error::new( + eyre!("Please call `start-cli net acme init` before adding a domain"), + ErrorKind::InvalidRequest, + ) + })? + .as_domains_mut() + .mutate(|domains| { + domains.insert(domain); + Ok(()) + }) + }) + .await?; + Ok(()) +} + +pub async fn remove_domain( + ctx: RpcContext, + DomainParams { domain }: DomainParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if let Some(acme) = db + .as_public_mut() + .as_server_info_mut() + .as_acme_mut() + .transpose_mut() + { + acme.as_domains_mut().mutate(|domains| { + domains.remove(&domain); + Ok(()) + }) + } else { + Ok(()) + } + }) + .await?; + Ok(()) +} + +pub async fn list_domains(ctx: RpcContext) -> Result, Error> { + if let Some(acme) = ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_acme() + .transpose() + { + acme.into_domains().de() + } else { + Ok(BTreeSet::new()) + } +} diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index 9b16441ce..05942ffa9 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -1,7 +1,9 @@ use std::fmt; use std::str::FromStr; +use clap::builder::ValueParserFactory; use imbl_value::InternedString; +use models::FromStrParser; use serde::{Deserialize, Serialize}; use torut::onion::OnionAddressV3; use ts_rs::TS; @@ -46,3 +48,10 @@ impl fmt::Display for HostAddress { } } } + +impl ValueParserFactory for HostAddress { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index 175fe3e83..be5db0f2d 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -1,10 +1,13 @@ use std::collections::{BTreeMap, BTreeSet}; +use clap::Parser; use imbl_value::InternedString; use models::{HostId, PackageId}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use ts_rs::TS; +use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; use crate::net::forward::AvailablePorts; use crate::net::host::address::HostAddress; @@ -134,3 +137,163 @@ impl Model { }) } } + +#[derive(Deserialize, Serialize, Parser)] +pub struct HostParams { + package: PackageId, +} + +pub fn host() -> ParentHandler { + ParentHandler::::new() + .subcommand( + "list", + from_fn_async(list_hosts) + .with_inherited(|HostParams { package }, _| package) + .with_custom_display_fn(|_, ids| { + for id in ids { + println!("{id}") + } + Ok(()) + }) + .with_about("List host IDs available for this service"), + ) + .subcommand( + "address", + address::().with_inherited(|HostParams { package }, _| package), + ) +} + +pub async fn list_hosts( + ctx: RpcContext, + _: Empty, + package: PackageId, +) -> Result, Error> { + ctx.db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package) + .or_not_found(&package)? + .into_hosts() + .keys() +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddressApiParams { + host: HostId, +} + +pub fn address() -> ParentHandler { + ParentHandler::::new() + .subcommand( + "add", + from_fn_async(add_address) + .with_inherited(|AddressApiParams { host }, package| (package, host)) + .no_display() + .with_about("Add an address to this host") + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_address) + .with_inherited(|AddressApiParams { host }, package| (package, host)) + .no_display() + .with_about("Remove an address from this host") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_addresses) + .with_inherited(|AddressApiParams { host }, package| (package, host)) + .with_custom_display_fn(|_, res| { + for address in res { + println!("{address}") + } + Ok(()) + }) + .with_about("List addresses for this host") + .with_call_remote::(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddressParams { + pub address: HostAddress, +} + +pub async fn add_address( + ctx: RpcContext, + AddressParams { address }: AddressParams, + (package, host): (PackageId, HostId), +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if let HostAddress::Onion { address } = address { + db.as_private() + .as_key_store() + .as_onion() + .get_key(&address)?; + } + + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package) + .or_not_found(&package)? + .as_hosts_mut() + .as_idx_mut(&host) + .or_not_found(&host)? + .as_addresses_mut() + .mutate(|a| Ok(a.insert(address))) + }) + .await?; + let service = ctx.services.get(&package).await; + let service_ref = service.as_ref().or_not_found(&package)?; + service_ref.update_host(host).await?; + + Ok(()) +} + +pub async fn remove_address( + ctx: RpcContext, + AddressParams { address }: AddressParams, + (package, host): (PackageId, HostId), +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package) + .or_not_found(&package)? + .as_hosts_mut() + .as_idx_mut(&host) + .or_not_found(&host)? + .as_addresses_mut() + .mutate(|a| Ok(a.remove(&address))) + }) + .await?; + let service = ctx.services.get(&package).await; + let service_ref = service.as_ref().or_not_found(&package)?; + service_ref.update_host(host).await?; + + Ok(()) +} + +pub async fn list_addresses( + ctx: RpcContext, + _: Empty, + (package, host): (PackageId, HostId), +) -> Result, Error> { + ctx.db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package) + .or_not_found(&package)? + .into_hosts() + .into_idx(&host) + .or_not_found(&host)? + .into_addresses() + .de() +} diff --git a/core/startos/src/net/keys.rs b/core/startos/src/net/keys.rs index 02ec17329..866b2ca06 100644 --- a/core/startos/src/net/keys.rs +++ b/core/startos/src/net/keys.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::account::AccountInfo; +use crate::net::acme::AcmeCertStore; use crate::net::ssl::CertStore; use crate::net::tor::OnionStore; use crate::prelude::*; @@ -10,13 +11,15 @@ use crate::prelude::*; pub struct KeyStore { pub onion: OnionStore, pub local_certs: CertStore, - // pub letsencrypt_certs: BTreeMap, CertData> + #[serde(default)] + pub acme: AcmeCertStore, } impl KeyStore { pub fn new(account: &AccountInfo) -> Result { let mut res = Self { onion: OnionStore::new(), local_certs: CertStore::new(account)?, + acme: AcmeCertStore::new(), }; res.onion.insert(account.tor_key.clone()); Ok(res) diff --git a/core/startos/src/net/mod.rs b/core/startos/src/net/mod.rs index 2735454e7..53b94454d 100644 --- a/core/startos/src/net/mod.rs +++ b/core/startos/src/net/mod.rs @@ -1,5 +1,6 @@ use rpc_toolkit::{Context, HandlerExt, ParentHandler}; +pub mod acme; pub mod dhcp; pub mod dns; pub mod forward; @@ -28,4 +29,8 @@ pub fn net() -> ParentHandler { "dhcp", dhcp::dhcp::().with_about("Command to update IP assigned from dhcp"), ) + .subcommand( + "acme", + acme::acme::().with_about("Setup automatic clearnet certificate acquisition"), + ) } diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index a94c7a7d0..a8beaf55f 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -261,7 +261,7 @@ impl NetService { errors.into_result() } - async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> { + pub async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> { let ctrl = self.net_controller()?; let mut hostname_info = BTreeMap::new(); let binds = self.binds.entry(id.clone()).or_default(); @@ -330,16 +330,29 @@ impl NetService { } HostAddress::Domain { address } => { if hostnames.insert(address.clone()) { + let address = Some(address.clone()); rcs.push( ctrl.vhost .add( - Some(address.clone()), + address.clone(), external, target, connect_ssl.clone(), ) .await?, ); + if ssl.preferred_external_port == 443 { + rcs.push( + ctrl.vhost + .add( + address.clone(), + 5443, + target, + connect_ssl.clone(), + ) + .await?, + ); + } } } } @@ -363,11 +376,32 @@ impl NetService { network_interface_id: interface.clone(), public: false, hostname: IpHostname::Local { - value: format!("{hostname}.local"), + value: InternedString::from_display(&{ + let hostname = &hostname; + lazy_format!("{hostname}.local") + }), port: new_lan_bind.0.assigned_port, ssl_port: new_lan_bind.0.assigned_ssl_port, }, }); + for address in host.addresses() { + if let HostAddress::Domain { address } = address { + if let Some(ssl) = &new_lan_bind.1 { + if ssl.preferred_external_port == 443 { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Domain { + domain: address.clone(), + subdomain: None, + port: None, + ssl_port: Some(443), + }, + }); + } + } + } + } if let Some(ipv4) = ip_info.ipv4 { bind_hostname_info.push(HostnameInfo::Ip { network_interface_id: interface.clone(), @@ -515,6 +549,7 @@ impl NetService { ctrl.tor.gc(Some(addr.clone()), None).await?; } } + self.net_controller()? .db .mutate(|db| { diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index b1824140b..ade10d959 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -47,13 +47,16 @@ pub enum IpHostname { ssl_port: Option, }, Local { - value: String, + #[ts(type = "string")] + value: InternedString, port: Option, ssl_port: Option, }, Domain { - domain: String, - subdomain: Option, + #[ts(type = "string")] + domain: InternedString, + #[ts(type = "string | null")] + subdomain: Option, port: Option, ssl_port: Option, }, diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index d93ce9302..bba50c371 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -26,7 +26,7 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::logs::{journalctl, LogSource, LogsParams}; use crate::prelude::*; -use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; +use crate::util::serde::{display_serializable, Base64, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; pub const SYSTEMD_UNIT: &str = "tor@default"; @@ -59,7 +59,9 @@ impl Model { self.insert(&key.public().get_onion_address(), &key) } pub fn get_key(&self, address: &OnionAddressV3) -> Result { - self.as_idx(address).or_not_found(address)?.de() + self.as_idx(address) + .or_not_found(lazy_format!("private key for {address}"))? + .de() } } @@ -108,7 +110,85 @@ pub fn tor() -> ParentHandler { .with_about("Reset Tor daemon") .with_call_remote::(), ) + .subcommand( + "key", + key::().with_about("Manage the onion service key store"), + ) } + +pub fn key() -> ParentHandler { + ParentHandler::new() + .subcommand( + "generate", + from_fn_async(generate_key) + .with_about("Generate an onion service key and add it to the key store") + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add_key) + .with_about("Add an onion service key to the key store") + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_keys) + .with_custom_display_fn(|_, res| { + for addr in res { + println!("{addr}"); + } + Ok(()) + }) + .with_about("List onion services with keys in the key store") + .with_call_remote::(), + ) +} + +pub async fn generate_key(ctx: RpcContext) -> Result { + ctx.db + .mutate(|db| { + Ok(db + .as_private_mut() + .as_key_store_mut() + .as_onion_mut() + .new_key()? + .public() + .get_onion_address()) + }) + .await +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct AddKeyParams { + pub key: Base64<[u8; 64]>, +} + +pub async fn add_key( + ctx: RpcContext, + AddKeyParams { key }: AddKeyParams, +) -> Result { + let key = TorSecretKeyV3::from(key.0); + ctx.db + .mutate(|db| { + db.as_private_mut() + .as_key_store_mut() + .as_onion_mut() + .insert_key(&key) + }) + .await?; + Ok(key.public().get_onion_address()) +} + +pub async fn list_keys(ctx: RpcContext) -> Result, Error> { + ctx.db + .peek() + .await + .into_private() + .into_key_store() + .into_onion() + .keys() +} + #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 9fc7c8384..7d48b1469 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; +use async_acme::acme::ACME_TLS_ALPN_NAME; use axum::body::Body; use axum::extract::Request; use axum::response::Response; @@ -15,31 +16,47 @@ use models::ResultExt; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::{watch, Mutex, RwLock}; +use tokio_rustls::rustls::crypto::CryptoProvider; use tokio_rustls::rustls::pki_types::{ CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, }; -use tokio_rustls::rustls::server::Acceptor; +use tokio_rustls::rustls::server::{Acceptor, ResolvesServerCert}; +use tokio_rustls::rustls::sign::CertifiedKey; use tokio_rustls::rustls::{RootCertStore, ServerConfig}; use tokio_rustls::{LazyConfigAcceptor, TlsConnector}; +use tokio_stream::wrappers::WatchStream; +use tokio_stream::StreamExt; use tracing::instrument; use ts_rs::TS; use crate::db::model::Database; +use crate::net::acme::AcmeCertCache; use crate::net::static_server::server_error; use crate::prelude::*; use crate::util::io::BackTrackingIO; +use crate::util::sync::SyncMutex; use crate::util::serde::MaybeUtf8String; +#[derive(Debug)] +struct SingleCertResolver(Arc); +impl ResolvesServerCert for SingleCertResolver { + fn resolve(&self, _: tokio_rustls::rustls::server::ClientHello) -> Option> { + Some(self.0.clone()) + } +} + // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 pub struct VHostController { + crypto_provider: Arc, db: TypedPatchDb, servers: Mutex>, } impl VHostController { pub fn new(db: TypedPatchDb) -> Self { Self { + crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), db, servers: Mutex::new(BTreeMap::new()), } @@ -56,7 +73,8 @@ impl VHostController { let server = if let Some(server) = writable.remove(&external) { server } else { - VHostServer::new(external, self.db.clone()).await? + tracing::info!("Listening on {external}"); + VHostServer::new(external, self.db.clone(), self.crypto_provider.clone()).await? }; let rc = server .add( @@ -108,7 +126,11 @@ struct VHostServer { } impl VHostServer { #[instrument(skip_all)] - async fn new(port: u16, db: TypedPatchDb) -> Result { + async fn new(port: u16, db: TypedPatchDb, crypto_provider: Arc) -> Result { + let acme_tls_alpn_cache = Arc::new(SyncMutex::new(BTreeMap::< + InternedString, + watch::Receiver>>, + >::new())); // check if port allowed let listener = TcpListener::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port)) .await @@ -133,9 +155,11 @@ impl VHostServer { let mut stream = BackTrackingIO::new(stream); let mapping = mapping.clone(); let db = db.clone(); + let acme_tls_alpn_cache = acme_tls_alpn_cache.clone(); + let crypto_provider = crypto_provider.clone(); tokio::spawn(async move { if let Err(e) = async { - let mid = match LazyConfigAcceptor::new( + let mid: tokio_rustls::StartHandshake<&mut BackTrackingIO> = match LazyConfigAcceptor::new( Acceptor::default(), &mut stream, ) @@ -206,38 +230,102 @@ impl VHostServer { .map(|(target, _)| target.clone()) }; if let Some(target) = target { - let mut tcp_stream = - TcpStream::connect(target.addr).await?; - let hostnames = target_name - .into_iter() - .chain( - db.peek() - .await - .into_public() - .into_server_info() - .into_ip_info() - .into_entries()? - .into_iter() - .flat_map(|(_, ips)| [ - ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), - ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) - ]) - .filter_map(|a| a.transpose()) - .map(|a| a.map(|ip| InternedString::from_display(&ip))) - .collect::, _>>()?, - ) - .collect(); - let key = db - .mutate(|v| { - v.as_private_mut() - .as_key_store_mut() - .as_local_certs_mut() - .cert_for(&hostnames) - }) - .await?; - let cfg = ServerConfig::builder() - .with_no_client_auth(); - let mut cfg = + let peek = db.peek().await; + let root = peek.as_private().as_key_store().as_local_certs().as_root_cert().de()?; + let mut cfg = match async { + if let Some(acme_settings) = peek.as_public().as_server_info().as_acme().de()? { + if let Some(domain) = target_name.as_ref().filter(|target_name| acme_settings.domains.contains(*target_name)) { + if mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .any(|alpn| alpn == ACME_TLS_ALPN_NAME) + { + let cert = WatchStream::new( + acme_tls_alpn_cache.peek(|c| c.get(&**domain).cloned()) + .ok_or_else(|| { + Error::new( + eyre!("No challenge recv available for {domain}"), + ErrorKind::OpenSsl + ) + })?, + ); + tracing::info!("Waiting for verification cert for {domain}"); + let cert = cert + .filter(|c| c.is_some()) + .next() + .await + .flatten() + .ok_or_else(|| { + Error::new(eyre!("No challenge available for {domain}"), ErrorKind::OpenSsl) + })?; + tracing::info!("Verification cert received for {domain}"); + let mut cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(cert))); + + cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()]; + return Ok(Err(cfg)); + } else { + let domains = [domain.to_string()]; + let (send, recv) = watch::channel(None); + acme_tls_alpn_cache.mutate(|c| c.insert(domain.clone(), recv)); + let cert = + async_acme::rustls_helper::order( + |_, cert| { + send.send_replace(Some(Arc::new(cert))); + Ok(()) + }, + acme_settings.provider.as_str(), + &domains, + Some(&AcmeCertCache(&db)), + &acme_settings.contact, + ) + .await + .with_kind(ErrorKind::OpenSsl)?; + return Ok(Ok( + ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth() + .with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert)))) + )); + } + } + } + let hostnames = target_name + .into_iter() + .chain( + peek + .as_public() + .as_server_info() + .as_ip_info() + .as_entries()? + .into_iter() + .flat_map(|(_, ips)| [ + ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), + ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) + ]) + .filter_map(|a| a.transpose()) + .map(|a| a.map(|ip| InternedString::from_display(&ip))) + .collect::, _>>()?, + ) + .collect(); + let key = db + .mutate(|v| { + v.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let cfg = ServerConfig::builder_with_provider(crypto_provider.clone()) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? + .with_no_client_auth(); if mid.client_hello().signature_schemes().contains( &tokio_rustls::rustls::SignatureScheme::ED25519, ) { @@ -275,16 +363,34 @@ impl VHostServer { )), ) } - .with_kind(crate::ErrorKind::OpenSsl)?; + .with_kind(crate::ErrorKind::OpenSsl) + .map(Ok) + }.await? { + Ok(a) => a, + Err(cfg) => { + tracing::info!("performing ACME auth challenge"); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; + accept.await?; + tracing::info!("ACME auth challenge completed"); + return Ok(()); + } + }; + let mut tcp_stream = + TcpStream::connect(target.addr).await?; match target.connect_ssl { Ok(()) => { let mut client_cfg = - tokio_rustls::rustls::ClientConfig::builder() + tokio_rustls::rustls::ClientConfig::builder_with_provider(crypto_provider) + .with_safe_default_protocol_versions() + .with_kind(crate::ErrorKind::OpenSsl)? .with_root_certificates({ let mut store = RootCertStore::empty(); store.add( CertificateDer::from( - key.root.to_der()?, + root.to_der()?, ), ).with_kind(crate::ErrorKind::OpenSsl)?; store diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 078631252..d73c51beb 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -16,7 +16,7 @@ use futures::stream::FusedStream; use futures::{SinkExt, StreamExt, TryStreamExt}; use imbl_value::{json, InternedString}; use itertools::Itertools; -use models::{ActionId, ImageId, PackageId, ProcedureName}; +use models::{ActionId, HostId, ImageId, PackageId, ProcedureName}; use nix::sys::signal::Signal; use persistent_container::{PersistentContainer, Subcontainer}; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; @@ -603,6 +603,30 @@ impl Service { memory_usage: MiB::from_MiB(used), }) } + + pub async fn update_host(&self, host_id: HostId) -> Result<(), Error> { + let host = self + .seed + .ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(&self.seed.id) + .or_not_found(&self.seed.id)? + .as_hosts() + .as_idx(&host_id) + .or_not_found(&host_id)? + .de()?; + self.seed + .persistent_container + .net_service + .lock() + .await + .update(host_id, host) + .await + } } #[derive(Debug, Clone)] diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 8f4bf4c15..5b785ce9a 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1029,6 +1029,12 @@ impl>> FromStr for Base64 { }) } } +impl>> ValueParserFactory for Base64 { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} impl<'de, T: TryFrom>> Deserialize<'de> for Base64 { fn deserialize(deserializer: D) -> Result where @@ -1215,6 +1221,30 @@ impl PemEncoding for ed25519_dalek::SigningKey { } } +#[derive(Clone, Debug)] +pub struct Pkcs8Doc { + pub tag: String, + pub document: pkcs8::Document, +} + +impl PemEncoding for Pkcs8Doc { + fn from_pem(pem: &str) -> Result { + let (tag, document) = pkcs8::Document::from_pem(pem).map_err(E::custom)?; + Ok(Pkcs8Doc { + tag: tag.into(), + document, + }) + } + fn to_pem(&self) -> Result { + der::pem::encode_string( + &self.tag, + pkcs8::LineEnding::default(), + self.document.as_bytes(), + ) + .map_err(E::custom) + } +} + pub mod pem { use serde::{Deserialize, Deserializer, Serializer}; diff --git a/sdk/base/lib/osBindings/AcmeSettings.ts b/sdk/base/lib/osBindings/AcmeSettings.ts new file mode 100644 index 000000000..bdf151ec7 --- /dev/null +++ b/sdk/base/lib/osBindings/AcmeSettings.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AcmeSettings = { + provider: string + /** + * email addresses for letsencrypt + */ + contact: Array + /** + * domains to get letsencrypt certs for + */ + domains: string[] +} diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index 435bf874a..89d7fc1b0 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AcmeSettings } from "./AcmeSettings" import type { Governor } from "./Governor" import type { IpInfo } from "./IpInfo" import type { LshwDevice } from "./LshwDevice" @@ -22,6 +23,7 @@ export type ServerInfo = { */ torAddress: string ipInfo: { [key: string]: IpInfo } + acme: AcmeSettings | null statusInfo: ServerStatus wifi: WifiInfo unreadNotificationCount: number diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index acd555219..f76f595c9 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -1,4 +1,5 @@ export { AcceptSigners } from "./AcceptSigners" +export { AcmeSettings } from "./AcmeSettings" export { ActionId } from "./ActionId" export { ActionInput } from "./ActionInput" export { ActionMetadata } from "./ActionMetadata" diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 797fde614..aea6b5828 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -56,6 +56,7 @@ export const mockPatchData: DataModel = { ipv6Range: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64', }, }, + acme: null, unreadNotificationCount: 4, // password is asdfasdf passwordHash: