From ab73183b2bdeaaa28584404074c46f3584b4a097 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:27:14 -0800 Subject: [PATCH 01/25] Add ability to build GTK app AppImage (#240) #238 Add AppImage build support Implements - Downgrade to libadwaita 1.3 for wider distro support - Add build script, workflow, and docs for AppImage - Add build docs for Debian (apt) and Void Linux - Building AppImage in CI --- .github/workflows/build-appimage.yml | 23 ++ .github/workflows/build-flatpak.yml | 5 +- burrow-gtk/Cargo.lock | 255 ++++++++++--------- burrow-gtk/Cargo.toml | 2 +- burrow-gtk/build-aux/Dockerfile | 18 ++ burrow-gtk/build-aux/build_appimage.sh | 28 ++ burrow-gtk/meson.build | 4 +- burrow-gtk/src/components/app.rs | 22 +- burrow-gtk/src/components/settings_screen.rs | 2 +- burrow-gtk/src/components/switch_screen.rs | 2 +- docs/GTK_APP.md | 97 ++++++- 11 files changed, 311 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/build-appimage.yml create mode 100644 burrow-gtk/build-aux/Dockerfile create mode 100755 burrow-gtk/build-aux/build_appimage.sh diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml new file mode 100644 index 0000000..ef5c525 --- /dev/null +++ b/.github/workflows/build-appimage.yml @@ -0,0 +1,23 @@ +name: Build AppImage +on: + push: + branches: [main] + pull_request: +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - uses: actions/checkout@v4 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - uses: actions/upload-artifact@v4 + with: + name: AppImage + path: Burrow-x86_64.AppImage + diff --git a/.github/workflows/build-flatpak.yml b/.github/workflows/build-flatpak.yml index e0e804e..d74eec3 100644 --- a/.github/workflows/build-flatpak.yml +++ b/.github/workflows/build-flatpak.yml @@ -1,7 +1,4 @@ -on: - push: - branches: [main] - pull_request: +on: workflow_dispatch name: Build Flatpak jobs: flatpak: diff --git a/burrow-gtk/Cargo.lock b/burrow-gtk/Cargo.lock index d0b7009..6721318 100644 --- a/burrow-gtk/Cargo.lock +++ b/burrow-gtk/Cargo.lock @@ -257,16 +257,16 @@ dependencies = [ "caps", "chacha20poly1305", "clap", - "env_logger", + "console", "fehler", "futures", "hmac", "ip_network", "ip_network_table", - "ipnet", "libsystemd", "log", - "nix", + "nix 0.27.1", + "once_cell", "parking_lot", "rand", "rand_core", @@ -281,7 +281,6 @@ dependencies = [ "tracing-oslog", "tracing-subscriber", "tun", - "uuid", "x25519-dalek", ] @@ -332,11 +331,11 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -346,9 +345,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.18.2" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1" dependencies = [ "glib-sys", "libc", @@ -501,6 +500,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -617,6 +629,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -626,19 +644,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -730,13 +735,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "nanorand", + "pin-project", "spin", ] @@ -867,10 +873,11 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" dependencies = [ + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -880,9 +887,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb" dependencies = [ "gio-sys", "glib-sys", @@ -893,10 +900,11 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6" +checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk4-sys", @@ -908,9 +916,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.7.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0" +checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -974,10 +982,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gio" -version = "0.18.4" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" dependencies = [ + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -993,9 +1002,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" dependencies = [ "glib-sys", "gobject-sys", @@ -1006,11 +1015,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -1035,23 +1044,24 @@ checksum = "3431c56f463443cba9bc3600248bc6d680cb614c2ee1cdd39dab5415bd12ac5c" [[package]] name = "glib-macros" -version = "0.18.5" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ + "anyhow", "heck", - "proc-macro-crate 2.0.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", + "syn 1.0.109", ] [[package]] name = "glib-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" dependencies = [ "libc", "system-deps", @@ -1065,9 +1075,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" dependencies = [ "glib-sys", "libc", @@ -1076,9 +1086,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" +checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9" dependencies = [ "glib", "graphene-sys", @@ -1087,9 +1097,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.18.1" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" +checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d" dependencies = [ "glib-sys", "libc", @@ -1099,10 +1109,11 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e" +checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "gdk4", "glib", @@ -1114,9 +1125,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55" +checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -1130,10 +1141,11 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.7.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842" +checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" dependencies = [ + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1146,17 +1158,18 @@ dependencies = [ "gtk4-macros", "gtk4-sys", "libc", + "once_cell", "pango", ] [[package]] name = "gtk4-macros" -version = "0.7.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f" +checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f" dependencies = [ "anyhow", - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -1165,9 +1178,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.7.3" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8" +checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1277,12 +1290,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -1376,20 +1383,6 @@ name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -dependencies = [ - "serde", -] - -[[package]] -name = "is-terminal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.52.0", -] [[package]] name = "itoa" @@ -1429,10 +1422,11 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libadwaita" -version = "0.5.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c" +checksum = "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf" dependencies = [ + "bitflags 1.3.2", "gdk-pixbuf", "gdk4", "gio", @@ -1445,9 +1439,9 @@ dependencies = [ [[package]] name = "libadwaita-sys" -version = "0.5.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a" +checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404" dependencies = [ "gdk4-sys", "gio-sys", @@ -1487,14 +1481,14 @@ dependencies = [ [[package]] name = "libsystemd" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +checksum = "c592dc396b464005f78a5853555b9f240bc5378bf5221acc4e129910b2678869" dependencies = [ "hmac", "libc", "log", - "nix", + "nix 0.27.1", "nom", "once_cell", "serde", @@ -1675,6 +1669,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "memoffset 0.9.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -1807,10 +1813,11 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" -version = "0.18.3" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" dependencies = [ + "bitflags 1.3.2", "gio", "glib", "libc", @@ -1820,9 +1827,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.18.0" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195" dependencies = [ "glib-sys", "gobject-sys", @@ -1894,6 +1901,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1961,16 +1988,6 @@ dependencies = [ "toml_edit 0.19.15", ] -[[package]] -name = "proc-macro-crate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" -dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2098,8 +2115,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relm4" -version = "0.7.0-beta.2" -source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c16f3fad883034773b7f5af4d7e865532b8f3641e5a8bab2a34561a8d960d81" dependencies = [ "async-trait", "flume", @@ -2115,8 +2133,9 @@ dependencies = [ [[package]] name = "relm4-macros" -version = "0.7.0-beta.2" -source = "git+https://github.com/Relm4/Relm4#e189eee06b887470e0fd65cbaf6d7c0161bed5ea" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9340e2553c0a184a80a0bfa1dcf73c47f3d48933aa6be90724b202f9fbd24735" dependencies = [ "proc-macro2", "quote", @@ -2547,15 +2566,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.56" @@ -2633,6 +2643,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -2839,7 +2850,7 @@ dependencies = [ "libc", "libloading 0.7.4", "log", - "nix", + "nix 0.26.4", "reqwest", "schemars", "serde", @@ -2925,7 +2936,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom", "serde", ] @@ -3078,15 +3088,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/burrow-gtk/Cargo.toml b/burrow-gtk/Cargo.toml index 244c161..21cb52e 100644 --- a/burrow-gtk/Cargo.toml +++ b/burrow-gtk/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0" -relm4 = { features = ["libadwaita", "gnome_45"], git = "https://github.com/Relm4/Relm4" } +relm4 = { version = "0.6", features = ["libadwaita", "gnome_44"]} burrow = { version = "*", path = "../burrow/" } tokio = { version = "1.35.0", features = ["time", "sync"] } gettext-rs = { version = "0.7.0", features = ["gettext-system"] } diff --git a/burrow-gtk/build-aux/Dockerfile b/burrow-gtk/build-aux/Dockerfile new file mode 100644 index 0000000..df07c4a --- /dev/null +++ b/burrow-gtk/build-aux/Dockerfile @@ -0,0 +1,18 @@ +FROM fedora:39 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -eux && \ + dnf update -y && \ + dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal +ENV PATH="/root/.cargo/bin:${PATH}" + +WORKDIR /app +COPY . /app + +RUN cd /app/burrow-gtk/ && \ + ./build-aux/build_appimage.sh + + diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh new file mode 100755 index 0000000..248cca7 --- /dev/null +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -ex + +BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)" +BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage" +LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}" + +if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then + echo "Make sure to cd into burrow-gtk" + exit 1 +fi + +ARCHITECTURE=$(lscpu | grep Architecture | awk '{print $2}') + +if [ "$ARCHITECTURE" == "x86_64" ]; then + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/$LINUXDEPLOY_VERSION/linuxdeploy-x86_64.AppImage" -o /dev/null -O /tmp/linuxdeploy + chmod a+x /tmp/linuxdeploy +elif [ "$ARCHITECTURE" == "aarch64" ]; then + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/$LINUXDEPLOY_VERSION/linuxdeploy-aarch64.AppImage" -o /dev/null -O /tmp/linuxdeploy + chmod a+x /tmp/linuxdeploy +fi + +meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr +meson compile -C $BURROW_GTK_BUILD +DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD +/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage +mv *.AppImage $BURROW_GTK_BUILD diff --git a/burrow-gtk/meson.build b/burrow-gtk/meson.build index 70d8403..8c2d5c1 100644 --- a/burrow-gtk/meson.build +++ b/burrow-gtk/meson.build @@ -34,8 +34,8 @@ i18n = import('i18n') gnome = import('gnome') # External Dependencies -dependency('gtk4', version: '>= 4.12') -dependency('libadwaita-1', version: '>= 1.4') +dependency('gtk4', version: '>= 4.0') +dependency('libadwaita-1', version: '>= 1.2') glib_compile_resources = find_program('glib-compile-resources', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true) diff --git a/burrow-gtk/src/components/app.rs b/burrow-gtk/src/components/app.rs index b42b718..57348ef 100644 --- a/burrow-gtk/src/components/app.rs +++ b/burrow-gtk/src/components/app.rs @@ -81,16 +81,28 @@ impl AsyncComponent for App { let view_switcher_bar = adw::ViewSwitcherBar::builder().stack(&view_stack).build(); view_switcher_bar.set_reveal(true); - let toolbar = adw::ToolbarView::new(); - toolbar.add_top_bar( + // When libadwaita 1.4 support becomes more avaliable, this approach is more appropriate + // + // let toolbar = adw::ToolbarView::new(); + // toolbar.add_top_bar( + // &adw::HeaderBar::builder() + // .title_widget(>k::Label::new(Some("Burrow"))) + // .build(), + // ); + // toolbar.add_bottom_bar(&view_switcher_bar); + // toolbar.set_content(Some(&view_stack)); + // root.set_content(Some(&toolbar)); + + let content = gtk::Box::new(gtk::Orientation::Vertical, 0); + content.append( &adw::HeaderBar::builder() .title_widget(>k::Label::new(Some("Burrow"))) .build(), ); - toolbar.add_bottom_bar(&view_switcher_bar); - toolbar.set_content(Some(&view_stack)); + content.append(&view_stack); + content.append(&view_switcher_bar); - root.set_content(Some(&toolbar)); + root.set_content(Some(&content)); sender.input(AppMsg::PostInit); diff --git a/burrow-gtk/src/components/settings_screen.rs b/burrow-gtk/src/components/settings_screen.rs index 778eb84..0a29e43 100644 --- a/burrow-gtk/src/components/settings_screen.rs +++ b/burrow-gtk/src/components/settings_screen.rs @@ -21,7 +21,7 @@ impl SimpleComponent for SettingsScreen { fn init( init: Self::Init, - root: Self::Root, + root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { let diag_group = settings::DiagGroup::builder() diff --git a/burrow-gtk/src/components/switch_screen.rs b/burrow-gtk/src/components/switch_screen.rs index a296c09..f660536 100644 --- a/burrow-gtk/src/components/switch_screen.rs +++ b/burrow-gtk/src/components/switch_screen.rs @@ -29,7 +29,7 @@ impl AsyncComponent for SwitchScreen { view! { gtk::Box { set_orientation: gtk::Orientation::Vertical, - set_valign: Align::BaselineFill, + set_valign: Align::Fill, gtk::Box { set_orientation: gtk::Orientation::Vertical, diff --git a/docs/GTK_APP.md b/docs/GTK_APP.md index 9b103a3..ef73d2b 100644 --- a/docs/GTK_APP.md +++ b/docs/GTK_APP.md @@ -1,22 +1,79 @@ # Linux GTK App Getting Started +Currently, the GTK App can be built as a binary or as an AppImage. +Note that the flatpak version can compile but will not run properly! + ## Dependencies ### Install Build Dependencies
- Fedora + Debian - 1. Install build dependencies + > Note: Burrow currently cannot compile on Debian Stable (Bookworm) due to its outdated dependencies + + 1. Install build dependencies ``` - sudo dnf install clang ninja cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib + sudo apt install -y clang meson cmake pkg-config libgtk-4-dev libadwaita-1-dev gettext desktop-file-utils ``` 2. Install flatpak builder (Optional) ``` - sudo dnf install flatpak-builder + sudo apt install -y flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo apt install -y wget fuse file + ``` + +
+ +
+ Fedora + + 1. Install build dependencies + + ``` + sudo dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib + ``` + + 2. Install flatpak builder (Optional) + + ``` + sudo dnf install -y flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo dnf install -y util-linux wget fuse fuse-libs file + ``` + +
+ +
+ Void Linux (glibc) + + 1. Install build dependencies + + ``` + sudo xbps-install -Sy gcc clang meson cmake pkg-config gtk4-devel gettext desktop-file-utils gtk4-update-icon-cache appstream-glib + ``` + + 2. Install flatpak builder (Optional) + + ``` + sudo xbps-install -Sy flatpak-builder + ``` + + 3. Install AppImage build tools (Optional) + + ``` + sudo xbps-install -Sy wget fuse file ```
@@ -51,10 +108,10 @@ flatpak install --user \
Flatpak - 1. Compile and install the flatpak + 1. Compile and install the flatpak ``` - flatpak-builder + flatpak-builder --user --install --force-clean --disable-rofiles-fuse \ flatpak_debug/ \ burrow-gtk/build-aux/com.hackclub.burrow.devel.json @@ -62,6 +119,23 @@ flatpak install --user \
+
+ AppImage + + 1. Enter the `burrow-gtk` + + ```bash + cd burrow-gtk + ``` + + 2. Compile the AppImage + + ``` + ./build-aux/build_appimage.sh + ``` + +
+ ## Running @@ -83,3 +157,14 @@ flatpak install --user \ ``` + +
+ AppImage + + The compiled binary can be found in `build-appimage/Burrow-*.AppImage`. + + ``` + ./build-appimage/Burrow-*.AppImage + ``` + +
From cca59992147479925a0dbd08dc9bccc80d10d5a3 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:34:15 -0800 Subject: [PATCH 02/25] Bump Rust version in Dockerfile clap-rs recently bumped their MSRV to 1.74 breaking the docker build. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b1500bb..3c12d45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.70.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.74.0-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 From 2088ae6ede685880ae3f18401812a34e5c0253b9 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Fri, 2 Feb 2024 14:48:13 +0800 Subject: [PATCH 03/25] Add Support for IPV6 and Arbitrary Server Address Add IPV6 support for Apple Devices Note: Works in GUI not CLI Adds Support for Arbitrary Server Address --- Apple/NetworkExtension/DataTypes.swift | 4 +- .../PacketTunnelProvider.swift | 21 +++- Cargo.lock | 16 +-- Dockerfile | 2 +- Makefile | 25 ++++- burrow-server-compose.yml | 38 +++++++ burrow/src/daemon/response.rs | 4 +- ...ommand__daemoncommand_serialization-2.snap | 2 +- ..._command__daemoncommand_serialization.snap | 2 +- ...n__response__response_serialization-4.snap | 2 +- burrow/src/main.rs | 2 +- burrow/src/tracing.rs | 1 + burrow/src/wireguard/config.rs | 10 +- server_patch.txt | 21 ++++ tun/Cargo.toml | 2 +- tun/src/options.rs | 6 +- tun/src/unix/apple/kern_control.rs | 2 +- tun/src/unix/apple/mod.rs | 55 +++++++-- tun/src/unix/apple/sys.rs | 104 +++++++++++++++++- tun/tests/packets.rs | 13 +++ 20 files changed, 276 insertions(+), 56 deletions(-) create mode 100644 burrow-server-compose.yml create mode 100644 server_patch.txt diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift index 391bfed..1409fde 100644 --- a/Apple/NetworkExtension/DataTypes.swift +++ b/Apple/NetworkExtension/DataTypes.swift @@ -31,7 +31,7 @@ struct BurrowStartRequest: Codable { let no_pi: Bool let tun_excl: Bool let tun_retrieve: Bool - let address: String? + let address: [String] } struct StartOptions: Codable { let tun: TunOptions @@ -51,7 +51,7 @@ struct BurrowResult: Codable where T: Codable { struct ServerConfigData: Codable { struct InternalConfig: Codable { - let address: String? + let address: [String] let name: String? let mtu: Int32? } diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 9231676..bfdb34a 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -31,7 +31,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { command: BurrowStartRequest( Start: BurrowStartRequest.StartOptions( tun: BurrowStartRequest.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: nil + name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] ) ) ) @@ -46,12 +46,21 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { let cfig = from.ServerConfig - guard let addr = cfig.address else { - return nil - } - // Using a makeshift remote tunnel address let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") - nst.ipv4Settings = NEIPv4Settings(addresses: [addr], subnetMasks: ["255.255.255.0"]) + var v4Addresses = [String]() + var v6Addresses = [String]() + for addr in cfig.address { + if IPv4Address(addr) != nil { + v6Addresses.append(addr) + } + if IPv6Address(addr) != nil { + v4Addresses.append(addr) + } + } + nst.ipv4Settings = NEIPv4Settings(addresses: v4Addresses, subnetMasks: v4Addresses.map { _ in + "255.255.255.0" + }) + nst.ipv6Settings = NEIPv6Settings(addresses: v6Addresses, networkPrefixLengths: v6Addresses.map { _ in 64 }) logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } diff --git a/Cargo.lock b/Cargo.lock index 85f11e7..a75bd28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1074,7 +1074,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", "tower-service", "tracing", @@ -2114,16 +2114,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -2305,7 +2295,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2547,7 +2537,7 @@ dependencies = [ "reqwest", "schemars", "serde", - "socket2 0.4.10", + "socket2", "ssri", "tempfile", "tokio", diff --git a/Dockerfile b/Dockerfile index 3c12d45..9f54478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.74.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.76.0-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 diff --git a/Makefile b/Makefile index 18b4b27..97d2d5a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -tun_num := $(shell ifconfig | awk -F 'utun|[: ]' '/utun[0-9]/ {print $$2}' | tail -n 1) +tun := $(shell ifconfig -l | sed 's/ /\n/g' | grep utun | tail -n 1) cargo_console := RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features cargo_norm := RUST_BACKTRACE=1 RUST_LOG=debug cargo run @@ -19,15 +19,28 @@ start: test-dns: @sudo route delete 8.8.8.8 - @sudo route add 8.8.8.8 -interface utun$(tun_num) + @sudo route add 8.8.8.8 -interface $(tun) @dig @8.8.8.8 hackclub.com test-https: @sudo route delete 193.183.0.162 - @sudo route add 193.183.0.162 -interface utun$(tun_num) + @sudo route add 193.183.0.162 -interface $(tun) @curl -vv https://search.marginalia.nu +v4_target := 146.190.62.39 test-http: - @sudo route delete 146.190.62.39 - @sudo route add 146.190.62.39 -interface utun$(tun_num) - @curl -vv 146.190.62.39:80 + @sudo route delete ${v4_target} + @sudo route add ${v4_target} -interface $(tun) + @curl -vv ${v4_target}:80 + +test-ipv4: + @sudo route delete ${v4_target} + @sudo route add ${v4_target} -interface $(tun) + @ping ${v4_target} + +v6_target := 2001:4860:4860::8888 +test-ipv6: + @sudo route delete ${v6_target} + @sudo route -n add -inet6 ${v6_target} -interface $(tun) + @echo preparing + @sudo ping6 -v ${v6_target} diff --git a/burrow-server-compose.yml b/burrow-server-compose.yml new file mode 100644 index 0000000..4ba31ee --- /dev/null +++ b/burrow-server-compose.yml @@ -0,0 +1,38 @@ +version: "2.1" +networks: + wg6: + enable_ipv6: true + ipam: + driver: default + config: + - subnet: "aa:bb:cc:de::/64" +services: + burrow: + image: lscr.io/linuxserver/wireguard:latest + privileged: true + container_name: burrow_server + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - PUID=1000 + - PGID=1000 + - TZ=Asia/Calcutta + - SERVERURL=wg.burrow.rs + - SERVERPORT=51820 + - PEERS=10 + - PEERDNS=1.1.1.1 + - INTERNAL_SUBNET=10.13.13.0 + - ALLOWEDIPS=0.0.0.0/0, ::/0 + - PERSISTENTKEEPALIVE_PEERS=all + - LOG_CONFS=true #optional + volumes: + - ./config:/config + - /lib/modules:/lib/modules + ports: + - 51820:51820/udp + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.eth0.proxy_ndp=1 + restart: unless-stopped \ No newline at end of file diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/response.rs index 172d4c7..37ee5d9 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/response.rs @@ -57,7 +57,7 @@ impl TryFrom<&TunInterface> for ServerInfo { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct ServerConfig { - pub address: Option, + pub address: Vec, pub name: Option, pub mtu: Option, } @@ -65,7 +65,7 @@ pub struct ServerConfig { impl Default for ServerConfig { fn default() -> Self { Self { - address: Some("10.13.13.2".to_string()), // Dummy remote address + address: vec!["10.13.13.2".to_string()], // Dummy remote address name: None, mtu: None, } diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap index 0eb9096..f78eeaa 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization-2.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/command.rs expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions {\n tun: TunOptions { ..TunOptions::default() },\n })).unwrap()" --- -{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap index bfd5117..eee563d 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__command__daemoncommand_serialization.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/command.rs expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" --- -{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":null}}} +{"Start":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap index 9752ebc..0b9385c 100644 --- a/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap +++ b/burrow/src/daemon/snapshots/burrow__daemon__response__response_serialization-4.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/response.rs expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" --- -{"result":{"Ok":{"ServerConfig":{"address":"10.13.13.2","name":null,"mtu":null}}},"id":0} +{"result":{"Ok":{"ServerConfig":{"address":["10.13.13.2"],"name":null,"mtu":null}}},"id":0} diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 79bb70b..71d1c02 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -55,7 +55,7 @@ async fn try_start() -> Result<()> { let mut client = DaemonClient::new().await?; client .send_command(DaemonCommand::Start(DaemonStartOptions { - tun: TunOptions::new().address("10.13.13.2"), + tun: TunOptions::new().address(vec!["10.13.13.2", "::2"]), })) .await .map(|_| ()) diff --git a/burrow/src/tracing.rs b/burrow/src/tracing.rs index 279f45d..861b41f 100644 --- a/burrow/src/tracing.rs +++ b/burrow/src/tracing.rs @@ -39,6 +39,7 @@ pub fn initialize() { tracing_subscriber::fmt::layer() .with_level(true) .with_writer(std::io::stderr) + .with_line_number(true) .compact() .with_filter(EnvFilter::from_default_env()) }); diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index afe7499..ed7b3cd 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -42,7 +42,7 @@ pub struct Peer { pub struct Interface { pub private_key: String, - pub address: String, + pub address: Vec, pub listen_port: u32, pub dns: Vec, pub mtu: Option, @@ -93,8 +93,8 @@ impl Default for Config { fn default() -> Self { Self { interface: Interface { - private_key: "GNqIAOCRxjl/cicZyvkvpTklgQuUmGUIEkH7IXF/sEE=".into(), - address: "10.13.13.2/24".into(), + private_key: "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=".into(), + address: vec!["10.13.13.2/24".into()], listen_port: 51820, dns: Default::default(), mtu: Default::default(), @@ -102,8 +102,8 @@ impl Default for Config { peers: vec![Peer { endpoint: "wg.burrow.rs:51820".into(), allowed_ips: vec!["8.8.8.8/32".into(), "0.0.0.0/0".into()], - public_key: "uy75leriJay0+oHLhRMpV+A5xAQ0hCJ+q7Ww81AOvT4=".into(), - preshared_key: Some("s7lx/mg+reVEMnGnqeyYOQkzD86n2+gYnx1M9ygi08k=".into()), + public_key: "8GaFjVO6c4luCHG4ONO+1bFG8tO+Zz5/Gy+Geht1USM=".into(), + preshared_key: Some("ha7j4BjD49sIzyF9SNlbueK0AMHghlj6+u0G3bzC698=".into()), persistent_keepalive: Default::default(), name: Default::default(), }], diff --git a/server_patch.txt b/server_patch.txt new file mode 100644 index 0000000..de8e14c --- /dev/null +++ b/server_patch.txt @@ -0,0 +1,21 @@ +# Add this to ~/server/wg0.conf upon regeneration + +PostUp = iptables -A FORWARD -i %i -j ACCEPT + +PostUp = iptables -A FORWARD -o %i -j ACCEPT + +PostUp = iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE + +PostUp = ip6tables -A FORWARD -i %i -j ACCEPT + +PostUp = ip6tables -A FORWARD -o %i -j ACCEPT + +PostDown = iptables -D FORWARD -i %i -j ACCEPT + +PostDown = iptables -D FORWARD -o %i -j ACCEPT + +PostDown = iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE + +PostDown = ip6tables -D FORWARD -i %i -j ACCEPT + +PostDown = ip6tables -D FORWARD -o %i -j ACCEPT \ No newline at end of file diff --git a/tun/Cargo.toml b/tun/Cargo.toml index e67e45f..7413f65 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" libc = "0.2" fehler = "1.0" nix = { version = "0.26", features = ["ioctl"] } -socket2 = "0.4" +socket2 = "0.5" tokio = { version = "1.28", features = [] } byteorder = "1.4" tracing = "0.1" diff --git a/tun/src/options.rs b/tun/src/options.rs index 339f71a..e21bf5f 100644 --- a/tun/src/options.rs +++ b/tun/src/options.rs @@ -21,7 +21,7 @@ pub struct TunOptions { /// (Apple) Retrieve the tun interface pub tun_retrieve: bool, /// (Linux) The IP address of the tun interface. - pub address: Option, + pub address: Vec, } impl TunOptions { @@ -44,8 +44,8 @@ impl TunOptions { self } - pub fn address(mut self, address: impl ToString) -> Self { - self.address = Some(address.to_string()); + pub fn address(mut self, address: Vec) -> Self { + self.address = address.iter().map(|x| x.to_string()).collect(); self } diff --git a/tun/src/unix/apple/kern_control.rs b/tun/src/unix/apple/kern_control.rs index 76e576f..6075233 100644 --- a/tun/src/unix/apple/kern_control.rs +++ b/tun/src/unix/apple/kern_control.rs @@ -21,7 +21,7 @@ impl SysControlSocket for socket2::Socket { unsafe { sys::resolve_ctl_info(self.as_raw_fd(), &mut info as *mut sys::ctl_info)? }; let (_, addr) = unsafe { - socket2::SockAddr::init(|addr_storage, len| { + socket2::SockAddr::try_init(|addr_storage, len| { *len = size_of::() as u32; let addr: &mut sys::sockaddr_ctl = &mut *addr_storage.cast(); diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 2787cde..6e859ca 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,13 +1,11 @@ -use std::{ - io::{Error, IoSlice}, - mem, - net::{Ipv4Addr, SocketAddrV4}, - os::fd::{AsRawFd, FromRawFd, RawFd}, -}; +use std::{io::{Error, IoSlice}, mem, net::{Ipv4Addr, SocketAddrV4}, os::fd::{AsRawFd, FromRawFd, RawFd}, ptr}; +use std::net::{IpAddr, Ipv6Addr, SocketAddrV6}; +use std::ptr::addr_of; use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; -use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; +use libc::{c_char, iovec, writev, AF_INET, AF_INET6, sockaddr_in6}; +use nix::sys::socket::SockaddrIn6; use socket2::{Domain, SockAddr, Socket, Type}; use tracing::{self, instrument}; @@ -49,7 +47,7 @@ impl TunInterface { pub fn retrieve() -> Option { (3..100) .filter_map(|fd| unsafe { - let peer_addr = socket2::SockAddr::init(|storage, len| { + let peer_addr = socket2::SockAddr::try_init(|storage, len| { *len = mem::size_of::() as u32; libc::getpeername(fd, storage as *mut _, len); Ok(()) @@ -71,9 +69,12 @@ impl TunInterface { #[throws] fn configure(&self, options: TunOptions) { - if let Some(addr) = options.address { - if let Ok(addr) = addr.parse() { - self.set_ipv4_addr(addr)?; + for addr in options.address{ + if let Ok(addr) = addr.parse::() { + match addr { + IpAddr::V4(addr) => {self.set_ipv4_addr(addr)?} + IpAddr::V6(addr) => {self.set_ipv6_addr(addr)?} + } } } } @@ -117,6 +118,14 @@ impl TunInterface { iff } + #[throws] + #[instrument] + fn in6_ifreq(&self) -> sys::in6_ifreq { + let mut iff: sys::in6_ifreq = unsafe { mem::zeroed() }; + iff.ifr_name = string_to_ifname(&self.name()?); + iff + } + #[throws] #[instrument] pub fn set_ipv4_addr(&self, addr: Ipv4Addr) { @@ -136,6 +145,21 @@ impl TunInterface { Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)) } + #[throws] + pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { + // let addr = SockAddr::from(SocketAddrV6::new(addr, 0, 0, 0)); + // println!("addr: {:?}", addr); + // let mut iff = self.in6_ifreq()?; + // let sto = addr.as_storage(); + // let ifadddr_ptr: *const sockaddr_in6 = addr_of!(sto).cast(); + // iff.ifr_ifru.ifru_addr = unsafe { *ifadddr_ptr }; + // println!("ifru addr set"); + // println!("{:?}", sys::SIOCSIFADDR_IN6); + // self.perform6(|fd| unsafe { sys::if_set_addr6(fd, &iff) })?; + // tracing::info!("ipv6_addr_set"); + tracing::warn!("Setting IPV6 address on MacOS CLI mode is not supported yet."); + } + #[throws] fn perform(&self, perform: impl FnOnce(RawFd) -> Result) -> R { let span = tracing::info_span!("perform", fd = self.as_raw_fd()); @@ -145,6 +169,15 @@ impl TunInterface { perform(socket.as_raw_fd())? } + #[throws] + fn perform6(&self, perform: impl FnOnce(RawFd) -> Result) -> R { + let span = tracing::info_span!("perform6", fd = self.as_raw_fd()); + let _enter = span.enter(); + + let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?; + perform(socket.as_raw_fd())? + } + #[throws] #[instrument] pub fn mtu(&self) -> i32 { diff --git a/tun/src/unix/apple/sys.rs b/tun/src/unix/apple/sys.rs index b4d4a6a..d48d6ee 100644 --- a/tun/src/unix/apple/sys.rs +++ b/tun/src/unix/apple/sys.rs @@ -1,6 +1,6 @@ use std::mem; -use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr}; +use libc::{c_char, c_int, c_short, c_uint, c_ulong, sockaddr, sockaddr_in6, time_t}; pub use libc::{ c_void, sockaddr_ctl, @@ -23,6 +23,7 @@ pub const UTUN_CONTROL_NAME: &str = "com.apple.net.utun_control"; pub const UTUN_OPT_IFNAME: libc::c_int = 2; pub const MAX_KCTL_NAME: usize = 96; +pub const SCOPE6_ID_MAX: usize = 16; #[repr(C)] #[derive(Copy, Clone, Debug)] @@ -74,7 +75,107 @@ pub struct ifreq { pub ifr_ifru: ifr_ifru, } +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct in6_addrlifetime{ + pub ia6t_expire: time_t, + pub ia6t_preferred: time_t, + pub ia6t_vltime: u32, + pub ia6t_pltime: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct in6_ifstat { + pub ifs6_in_receive: u64, + pub ifs6_in_hdrerr: u64, + pub ifs6_in_toobig: u64, + pub ifs6_in_noroute: u64, + pub ifs6_in_addrerr: u64, + pub ifs6_in_protounknown: u64, + pub ifs6_in_truncated: u64, + pub ifs6_in_discard: u64, + pub ifs6_in_deliver: u64, + pub ifs6_out_forward: u64, + pub ifs6_out_request: u64, + pub ifs6_out_discard: u64, + pub ifs6_out_fragok: u64, + pub ifs6_out_fragfail: u64, + pub ifs6_out_fragcreat: u64, + pub ifs6_reass_reqd: u64, + pub ifs6_reass_ok: u64, + pub ifs6_atmfrag_rcvd: u64, + pub ifs6_reass_fail: u64, + pub ifs6_in_mcast: u64, + pub ifs6_out_mcast: u64, + pub ifs6_cantfoward_icmp6: u64, + pub ifs6_addr_expiry_cnt: u64, + pub ifs6_pfx_expiry_cnt: u64, + pub ifs6_defrtr_expiry_cnt: u64, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct icmp6_ifstat { + pub ifs6_in_msg: u64, + pub ifs6_in_error: u64, + pub ifs6_in_dstunreach: u64, + pub ifs6_in_adminprohib: u64, + pub ifs6_in_timeexceed: u64, + pub ifs6_in_paramprob: u64, + pub ifs6_in_pkttoobig: u64, + pub ifs6_in_echo: u64, + pub ifs6_in_echoreply: u64, + pub ifs6_in_routersolicit: u64, + pub ifs6_in_routeradvert: u64, + pub ifs6_in_neighborsolicit: u64, + pub ifs6_in_neighboradvert: u64, + pub ifs6_in_redirect: u64, + pub ifs6_in_mldquery: u64, + pub ifs6_in_mldreport: u64, + pub ifs6_in_mlddone: u64, + pub ifs6_out_msg: u64, + pub ifs6_out_error: u64, + pub ifs6_out_dstunreach: u64, + pub ifs6_out_adminprohib: u64, + pub ifs6_out_timeexceed: u64, + pub ifs6_out_paramprob: u64, + pub ifs6_out_pkttoobig: u64, + pub ifs6_out_echo: u64, + pub ifs6_out_echoreply: u64, + pub ifs6_out_routersolicit: u64, + pub ifs6_out_routeradvert: u64, + pub ifs6_out_neighborsolicit: u64, + pub ifs6_out_neighboradvert: u64, + pub ifs6_out_redirect: u64, + pub ifs6_out_mldquery: u64, + pub ifs6_out_mldreport: u64, + pub ifs6_out_mlddone: u64, +} + +#[repr(C)] +pub union ifr_ifru6 { + pub ifru_addr: sockaddr_in6, + pub ifru_dstaddr: sockaddr_in6, + pub ifru_flags: c_int, + pub ifru_flags6: c_int, + pub ifru_metric: c_int, + pub ifru_intval: c_int, + pub ifru_data: *mut c_char, + pub ifru_lifetime: in6_addrlifetime, // ifru_lifetime + pub ifru_stat: in6_ifstat, + pub ifru_icmp6stat: icmp6_ifstat, + pub ifru_scope_id: [u32; SCOPE6_ID_MAX] +} + +#[repr(C)] +pub struct in6_ifreq { + pub ifr_name: [c_char; IFNAMSIZ], + pub ifr_ifru: ifr_ifru6, +} + pub const SIOCSIFADDR: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); +pub const SIOCSIFADDR_IN6: c_ulong = request_code_write!(b'i', 12, mem::size_of::()); pub const SIOCGIFMTU: c_ulong = request_code_readwrite!(b'i', 51, mem::size_of::()); pub const SIOCSIFMTU: c_ulong = request_code_write!(b'i', 52, mem::size_of::()); pub const SIOCGIFNETMASK: c_ulong = request_code_readwrite!(b'i', 37, mem::size_of::()); @@ -97,5 +198,6 @@ ioctl_read_bad!(if_get_addr, libc::SIOCGIFADDR, ifreq); ioctl_read_bad!(if_get_mtu, SIOCGIFMTU, ifreq); ioctl_read_bad!(if_get_netmask, SIOCGIFNETMASK, ifreq); ioctl_write_ptr_bad!(if_set_addr, SIOCSIFADDR, ifreq); +ioctl_write_ptr_bad!(if_set_addr6, SIOCSIFADDR_IN6, in6_ifreq); ioctl_write_ptr_bad!(if_set_mtu, SIOCSIFMTU, ifreq); ioctl_write_ptr_bad!(if_set_netmask, SIOCSIFNETMASK, ifreq); diff --git a/tun/tests/packets.rs b/tun/tests/packets.rs index 28090a2..80c078b 100644 --- a/tun/tests/packets.rs +++ b/tun/tests/packets.rs @@ -1,4 +1,5 @@ use std::{io::Error, net::Ipv4Addr}; +use std::net::Ipv6Addr; use fehler::throws; use tun::TunInterface; @@ -33,3 +34,15 @@ fn write_packets() { let bytes_written = tun.send(&buf)?; assert_eq!(bytes_written, 1504); } + +#[test] +#[throws] +#[ignore = "requires interactivity"] +#[cfg(not(target_os = "windows"))] +fn set_ipv6() { + let tun = TunInterface::new()?; + println!("tun name: {:?}", tun.name()?); + let targ_addr: Ipv6Addr = "::1".parse().unwrap(); + println!("v6 addr: {:?}", targ_addr); + tun.set_ipv6_addr(targ_addr)?; +} \ No newline at end of file From 29d2bfae3faefe70af393896bd1cdfc9a4174611 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 19 Feb 2024 11:28:00 +0000 Subject: [PATCH 04/25] Remove redundant type annotation --- Apple/NetworkExtension/Client.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apple/NetworkExtension/Client.swift b/Apple/NetworkExtension/Client.swift index a924c29..e7c1bc8 100644 --- a/Apple/NetworkExtension/Client.swift +++ b/Apple/NetworkExtension/Client.swift @@ -5,7 +5,7 @@ import Network final class Client { let connection: NWConnection - private let logger: Logger = Logger.logger(for: Client.self) + private let logger = Logger.logger(for: Client.self) private var generator = SystemRandomNumberGenerator() convenience init() throws { From c4c342dc8b79872989a02b99fc21235dacf30eea Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sun, 11 Feb 2024 03:17:14 +0800 Subject: [PATCH 05/25] Add implementation for stop command This adds implementation for stopping the tunnel via the `Stop` command. --- .../PacketTunnelProvider.swift | 11 +++ Makefile | 3 + burrow/src/daemon/instance.rs | 26 +++---- burrow/src/wireguard/iface.rs | 73 ++++++++++++++----- burrow/src/wireguard/pcb.rs | 6 +- 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index bfdb34a..7073401 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -44,6 +44,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } + override func stopTunnel(with reason: NEProviderStopReason) async { + do { + let client = try Client() + let command = BurrowRequest(id: 0, command: "Stop") + let data = try await client.request(command, type: Response>.self) + self.logger.log("Stopped client.") + } catch { + self.logger.error("Failed to stop tunnel: \(error)") + } + } + private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { let cfig = from.ServerConfig let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") diff --git a/Makefile b/Makefile index 97d2d5a..d0c9bd9 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ daemon: start: @$(cargo_norm) start +stop: + @$(cargo_norm) stop + test-dns: @sudo route delete 8.8.8.8 @sudo route add 8.8.8.8 -interface $(tun) diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 34e9878..0d3e726 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -21,7 +21,7 @@ enum RunState { pub struct DaemonInstance { rx: async_channel::Receiver, sx: async_channel::Sender, - tun_interface: Option>>, + tun_interface: Arc>>, wg_interface: Arc>, wg_state: RunState, } @@ -36,7 +36,7 @@ impl DaemonInstance { rx, sx, wg_interface, - tun_interface: None, + tun_interface: Arc::new(RwLock::new(None)), wg_state: RunState::Idle, } } @@ -50,15 +50,15 @@ impl DaemonInstance { warn!("Got start, but tun interface already up."); } RunState::Idle => { - let tun_if = Arc::new(RwLock::new(st.tun.open()?)); + let tun_if = st.tun.open()?; + debug!("Setting tun on wg_interface"); + self.wg_interface.read().await.set_tun(tun_if).await; + debug!("tun set on wg_interface"); debug!("Setting tun_interface"); - self.tun_interface = Some(tun_if.clone()); + self.tun_interface = self.wg_interface.read().await.get_tun(); debug!("tun_interface set: {:?}", self.tun_interface); - debug!("Setting tun on wg_interface"); - self.wg_interface.write().await.set_tun(tun_if); - debug!("tun set on wg_interface"); debug!("Cloning wg_interface"); let tmp_wg = self.wg_interface.clone(); @@ -82,22 +82,18 @@ impl DaemonInstance { } Ok(DaemonResponseData::None) } - DaemonCommand::ServerInfo => match &self.tun_interface { + DaemonCommand::ServerInfo => match &self.tun_interface.read().await.as_ref() { None => Ok(DaemonResponseData::None), Some(ti) => { info!("{:?}", ti); Ok(DaemonResponseData::ServerInfo(ServerInfo::try_from( - ti.read().await.inner.get_ref(), + ti.inner.get_ref(), )?)) } }, DaemonCommand::Stop => { - if self.tun_interface.is_some() { - self.tun_interface = None; - info!("Daemon stopping tun interface."); - } else { - warn!("Got stop, but tun interface is not up.") - } + self.wg_interface.read().await.remove_tun().await; + self.wg_state = RunState::Idle; Ok(DaemonResponseData::None) } DaemonCommand::ServerConfig => { diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 620c96c..6097082 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -1,10 +1,11 @@ use std::{net::IpAddr, sync::Arc}; +use std::ops::Deref; use anyhow::Error; use fehler::throws; use futures::future::join_all; use ip_network_table::IpNetworkTable; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, Notify}; use tracing::{debug, error}; use tun::tokio::TunInterface; @@ -46,9 +47,21 @@ impl FromIterator for IndexedPcbs { } } +enum IfaceStatus { + Running, + Idle +} + pub struct Interface { - tun: Option>>, + tun: Arc>>, pcbs: Arc, + status: Arc>, + stop_notifier: Arc, +} + +async fn is_running(status: Arc>) -> bool { + let st = status.read().await; + matches!(st.deref(), IfaceStatus::Running) } impl Interface { @@ -60,35 +73,54 @@ impl Interface { .collect::>()?; let pcbs = Arc::new(pcbs); - Self { pcbs, tun: None } + Self { pcbs, tun: Arc::new(RwLock::new(None)), status: Arc::new(RwLock::new(IfaceStatus::Idle)), stop_notifier: Arc::new(Notify::new()) } } - pub fn set_tun(&mut self, tun: Arc>) { - self.tun = Some(tun); + pub async fn set_tun(&self, tun: TunInterface) { + debug!("Setting tun interface"); + self.tun.write().await.replace(tun); + let mut st = self.status.write().await; + *st = IfaceStatus::Running; + } + + pub fn get_tun(&self) -> Arc>> { + self.tun.clone() + } + + pub async fn remove_tun(&self){ + let mut st = self.status.write().await; + self.stop_notifier.notify_waiters(); + *st = IfaceStatus::Idle; } pub async fn run(&self) -> anyhow::Result<()> { let pcbs = self.pcbs.clone(); let tun = self .tun - .clone() - .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + .clone(); + let status = self.status.clone(); + let stop_notifier = self.stop_notifier.clone(); log::info!("Starting interface"); let outgoing = async move { - loop { + while is_running(status.clone()).await { let mut buf = [0u8; 3000]; let src = { - let src = match tun.read().await.recv(&mut buf[..]).await { - Ok(len) => &buf[..len], - Err(e) => { - error!("Failed to read from interface: {}", e); - continue - } + let t = tun.read().await; + let Some(_tun) = t.as_ref() else { + continue; }; - debug!("Read {} bytes from interface", src.len()); - src + tokio::select! { + _ = stop_notifier.notified() => continue, + pkg = _tun.recv(&mut buf[..]) => match pkg { + Ok(len) => &buf[..len], + Err(e) => { + error!("Failed to read from interface: {}", e); + continue + } + }, + } }; let dst_addr = match Tunnel::dst_address(src) { @@ -123,8 +155,7 @@ impl Interface { let mut tsks = vec![]; let tun = self .tun - .clone() - .ok_or(anyhow::anyhow!("tun interface does not exist"))?; + .clone(); let outgoing = tokio::task::spawn(outgoing); tsks.push(outgoing); debug!("preparing to spawn read tasks"); @@ -149,9 +180,10 @@ impl Interface { }; let pcb = pcbs.pcbs[i].clone(); + let status = self.status.clone(); let update_timers_tsk = async move { let mut buf = [0u8; 65535]; - loop { + while is_running(status.clone()).await { tokio::time::sleep(tokio::time::Duration::from_millis(250)).await; match pcb.update_timers(&mut buf).await { Ok(..) => (), @@ -164,8 +196,9 @@ impl Interface { }; let pcb = pcbs.pcbs[i].clone(); + let status = self.status.clone(); let reset_rate_limiter_tsk = async move { - loop { + while is_running(status.clone()).await { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; pcb.reset_rate_limiter().await; } diff --git a/burrow/src/wireguard/pcb.rs b/burrow/src/wireguard/pcb.rs index db57968..974d84e 100755 --- a/burrow/src/wireguard/pcb.rs +++ b/burrow/src/wireguard/pcb.rs @@ -54,7 +54,7 @@ impl PeerPcb { Ok(()) } - pub async fn run(&self, tun_interface: Arc>) -> Result<(), Error> { + pub async fn run(&self, tun_interface: Arc>>) -> Result<(), Error> { tracing::debug!("starting read loop for pcb... for {:?}", &self); let rid: i32 = random(); let mut buf: [u8; 3000] = [0u8; 3000]; @@ -106,12 +106,12 @@ impl PeerPcb { } TunnResult::WriteToTunnelV4(packet, addr) => { tracing::debug!("WriteToTunnelV4: {:?}, {:?}", packet, addr); - tun_interface.read().await.send(packet).await?; + tun_interface.read().await.as_ref().ok_or(anyhow::anyhow!("tun interface does not exist"))?.send(packet).await?; break } TunnResult::WriteToTunnelV6(packet, addr) => { tracing::debug!("WriteToTunnelV6: {:?}, {:?}", packet, addr); - tun_interface.read().await.send(packet).await?; + tun_interface.read().await.as_ref().ok_or(anyhow::anyhow!("tun interface does not exist"))?.send(packet).await?; break } } From c755f752a0a621c742011af183627c11eeca6ce7 Mon Sep 17 00:00:00 2001 From: David Zhong <91637806+davnotdev@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:52:59 -0800 Subject: [PATCH 06/25] Implement launching a local daemon (#261) Allow AppImage and non-systemd systems to launch a local burrow daemon. --- README.md | 3 +- burrow-gtk/build-aux/build_appimage.sh | 6 +- burrow-gtk/src/components/app.rs | 21 +++- burrow-gtk/src/components/mod.rs | 1 + .../src/components/settings/daemon_group.rs | 111 ++++++++++++++++++ .../src/components/settings/diag_group.rs | 16 +-- burrow-gtk/src/components/settings/mod.rs | 5 +- burrow-gtk/src/components/settings_screen.rs | 41 +++++-- burrow-gtk/src/diag.rs | 15 ++- 9 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 burrow-gtk/src/components/settings/daemon_group.rs diff --git a/README.md b/README.md index 7492039..89914d0 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ Burrow is an open source tool for burrowing through firewalls, built by teenager ## Contributing -Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! For more information on how to contribute, please see [CONTRIBUTING.md] +Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow! Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. The project structure is divided in the following folders: ``` Apple/ # Xcode project for burrow on macOS and iOS burrow/ # Higher-level API library for tun and tun-async +burrow-gtk/ # GTK project for burrow on Linux tun/ # Low-level interface to OS networking src/ tokio/ # Async/Tokio code diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh index 248cca7..cd58c17 100755 --- a/burrow-gtk/build-aux/build_appimage.sh +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -5,6 +5,7 @@ set -ex BURROW_GTK_ROOT="$(readlink -f $(dirname -- "$(readlink -f -- "$BASH_SOURCE")")/..)" BURROW_GTK_BUILD="$BURROW_GTK_ROOT/build-appimage" LINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION:-"1-alpha-20240109-1"}" +BURROW_BUILD_TYPE="${BURROW_BUILD_TYPE:-"release"}" if [ "$BURROW_GTK_ROOT" != $(pwd) ]; then echo "Make sure to cd into burrow-gtk" @@ -21,8 +22,9 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then chmod a+x /tmp/linuxdeploy fi -meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr +meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE meson compile -C $BURROW_GTK_BUILD DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD -/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir --output appimage +cargo b --$BURROW_BUILD_TYPE --manifest-path=../Cargo.toml +/tmp/linuxdeploy --appimage-extract-and-run --appdir $BURROW_GTK_BUILD/AppDir -e $BURROW_GTK_BUILD/../../target/$BURROW_BUILD_TYPE/burrow --output appimage mv *.AppImage $BURROW_GTK_BUILD diff --git a/burrow-gtk/src/components/app.rs b/burrow-gtk/src/components/app.rs index 57348ef..62c98c0 100644 --- a/burrow-gtk/src/components/app.rs +++ b/burrow-gtk/src/components/app.rs @@ -6,7 +6,7 @@ const RECONNECT_POLL_TIME: Duration = Duration::from_secs(5); pub struct App { daemon_client: Arc>>, - _settings_screen: Controller, + settings_screen: Controller, switch_screen: AsyncController, } @@ -109,7 +109,7 @@ impl AsyncComponent for App { let model = App { daemon_client, switch_screen, - _settings_screen: settings_screen, + settings_screen, }; AsyncComponentParts { model, widgets } @@ -132,14 +132,23 @@ impl AsyncComponent for App { disconnected_daemon_client = true; self.switch_screen .emit(switch_screen::SwitchScreenMsg::DaemonDisconnect); + self.settings_screen + .emit(settings_screen::SettingsScreenMsg::DaemonStateChange) } } if disconnected_daemon_client || daemon_client.is_none() { - *daemon_client = DaemonClient::new().await.ok(); - if daemon_client.is_some() { - self.switch_screen - .emit(switch_screen::SwitchScreenMsg::DaemonReconnect); + match DaemonClient::new().await { + Ok(new_daemon_client) => { + *daemon_client = Some(new_daemon_client); + self.switch_screen + .emit(switch_screen::SwitchScreenMsg::DaemonReconnect); + self.settings_screen + .emit(settings_screen::SettingsScreenMsg::DaemonStateChange) + } + Err(_e) => { + // TODO: Handle Error + } } } } diff --git a/burrow-gtk/src/components/mod.rs b/burrow-gtk/src/components/mod.rs index b1cc938..b134809 100644 --- a/burrow-gtk/src/components/mod.rs +++ b/burrow-gtk/src/components/mod.rs @@ -18,3 +18,4 @@ mod settings_screen; mod switch_screen; pub use app::*; +pub use settings::{DaemonGroupMsg, DiagGroupMsg}; diff --git a/burrow-gtk/src/components/settings/daemon_group.rs b/burrow-gtk/src/components/settings/daemon_group.rs new file mode 100644 index 0000000..3817ca6 --- /dev/null +++ b/burrow-gtk/src/components/settings/daemon_group.rs @@ -0,0 +1,111 @@ +use super::*; +use std::process::Command; + +#[derive(Debug)] +pub struct DaemonGroup { + system_setup: SystemSetup, + daemon_client: Arc>>, + already_running: bool, +} + +pub struct DaemonGroupInit { + pub daemon_client: Arc>>, + pub system_setup: SystemSetup, +} + +#[derive(Debug)] +pub enum DaemonGroupMsg { + LaunchLocal, + DaemonStateChange, +} + +#[relm4::component(pub, async)] +impl AsyncComponent for DaemonGroup { + type Init = DaemonGroupInit; + type Input = DaemonGroupMsg; + type Output = (); + type CommandOutput = (); + + view! { + #[name(group)] + adw::PreferencesGroup { + #[watch] + set_sensitive: + (model.system_setup == SystemSetup::AppImage || model.system_setup == SystemSetup::Other) && + !model.already_running, + set_title: "Local Daemon", + set_description: Some("Run Local Daemon"), + + gtk::Button { + set_label: "Launch", + connect_clicked => DaemonGroupMsg::LaunchLocal + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + // Should be impossible to panic here + let model = DaemonGroup { + system_setup: init.system_setup, + daemon_client: init.daemon_client.clone(), + already_running: init.daemon_client.lock().await.is_some(), + }; + + let widgets = view_output!(); + + AsyncComponentParts { model, widgets } + } + + async fn update( + &mut self, + msg: Self::Input, + _sender: AsyncComponentSender, + _root: &Self::Root, + ) { + match msg { + DaemonGroupMsg::LaunchLocal => { + let burrow_original_bin = std::env::vars() + .find(|(k, _)| k == "APPDIR") + .map(|(_, v)| v + "/usr/bin/burrow") + .unwrap_or("/usr/bin/burrow".to_owned()); + + let mut burrow_bin = + String::from_utf8(Command::new("mktemp").output().unwrap().stdout).unwrap(); + burrow_bin.pop(); + + let privileged_spawn_script = format!( + r#"TEMP=$(mktemp -p /root) +cp {} $TEMP +chmod +x $TEMP +setcap CAP_NET_BIND_SERVICE,CAP_NET_ADMIN+eip $TEMP +mv $TEMP /tmp/burrow-detached-daemon"#, + burrow_original_bin + ) + .replace('\n', "&&"); + + // TODO: Handle error condition + + Command::new("pkexec") + .arg("sh") + .arg("-c") + .arg(privileged_spawn_script) + .arg(&burrow_bin) + .output() + .unwrap(); + + Command::new("/tmp/burrow-detached-daemon") + .env("RUST_LOG", "debug") + .arg("daemon") + .spawn() + .unwrap(); + } + DaemonGroupMsg::DaemonStateChange => { + self.already_running = self.daemon_client.lock().await.is_some(); + } + } + } +} diff --git a/burrow-gtk/src/components/settings/diag_group.rs b/burrow-gtk/src/components/settings/diag_group.rs index be542cd..a15e0ea 100644 --- a/burrow-gtk/src/components/settings/diag_group.rs +++ b/burrow-gtk/src/components/settings/diag_group.rs @@ -1,11 +1,10 @@ use super::*; -use diag::{StatusTernary, SystemSetup}; #[derive(Debug)] pub struct DiagGroup { daemon_client: Arc>>, - init_system: SystemSetup, + system_setup: SystemSetup, service_installed: StatusTernary, socket_installed: StatusTernary, socket_enabled: StatusTernary, @@ -14,19 +13,20 @@ pub struct DiagGroup { pub struct DiagGroupInit { pub daemon_client: Arc>>, + pub system_setup: SystemSetup, } impl DiagGroup { async fn new(daemon_client: Arc>>) -> Result { - let setup = SystemSetup::new(); + let system_setup = SystemSetup::new(); let daemon_running = daemon_client.lock().await.is_some(); Ok(Self { - service_installed: setup.is_service_installed()?, - socket_installed: setup.is_socket_installed()?, - socket_enabled: setup.is_socket_enabled()?, + service_installed: system_setup.is_service_installed()?, + socket_installed: system_setup.is_socket_installed()?, + socket_enabled: system_setup.is_socket_enabled()?, daemon_running, - init_system: setup, + system_setup, daemon_client, }) } @@ -52,7 +52,7 @@ impl AsyncComponent for DiagGroup { adw::ActionRow { #[watch] - set_title: &format!("Init System: {}", model.init_system) + set_title: &format!("System Type: {}", model.system_setup) }, adw::ActionRow { #[watch] diff --git a/burrow-gtk/src/components/settings/mod.rs b/burrow-gtk/src/components/settings/mod.rs index 53f46d4..aa87db2 100644 --- a/burrow-gtk/src/components/settings/mod.rs +++ b/burrow-gtk/src/components/settings/mod.rs @@ -1,5 +1,8 @@ use super::*; +use diag::{StatusTernary, SystemSetup}; +mod daemon_group; mod diag_group; -pub use diag_group::{DiagGroup, DiagGroupInit}; +pub use daemon_group::{DaemonGroup, DaemonGroupInit, DaemonGroupMsg}; +pub use diag_group::{DiagGroup, DiagGroupInit, DiagGroupMsg}; diff --git a/burrow-gtk/src/components/settings_screen.rs b/burrow-gtk/src/components/settings_screen.rs index 0a29e43..971f262 100644 --- a/burrow-gtk/src/components/settings_screen.rs +++ b/burrow-gtk/src/components/settings_screen.rs @@ -1,17 +1,24 @@ use super::*; +use diag::SystemSetup; pub struct SettingsScreen { - _diag_group: AsyncController, + diag_group: AsyncController, + daemon_group: AsyncController, } pub struct SettingsScreenInit { pub daemon_client: Arc>>, } +#[derive(Debug, PartialEq, Eq)] +pub enum SettingsScreenMsg { + DaemonStateChange, +} + #[relm4::component(pub)] impl SimpleComponent for SettingsScreen { type Init = SettingsScreenInit; - type Input = (); + type Input = SettingsScreenMsg; type Output = (); view! { @@ -24,21 +31,41 @@ impl SimpleComponent for SettingsScreen { root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { + let system_setup = SystemSetup::new(); + let diag_group = settings::DiagGroup::builder() .launch(settings::DiagGroupInit { + system_setup, daemon_client: Arc::clone(&init.daemon_client), }) - .forward(sender.input_sender(), |_| ()); + .forward(sender.input_sender(), |_| { + SettingsScreenMsg::DaemonStateChange + }); + + let daemon_group = settings::DaemonGroup::builder() + .launch(settings::DaemonGroupInit { + system_setup, + daemon_client: Arc::clone(&init.daemon_client), + }) + .forward(sender.input_sender(), |_| { + SettingsScreenMsg::DaemonStateChange + }); let widgets = view_output!(); widgets.preferences.add(diag_group.widget()); + widgets.preferences.add(daemon_group.widget()); - let model = SettingsScreen { - _diag_group: diag_group, - }; + let model = SettingsScreen { diag_group, daemon_group }; ComponentParts { model, widgets } } - fn update(&mut self, _: Self::Input, _sender: ComponentSender) {} + fn update(&mut self, _: Self::Input, _sender: ComponentSender) { + // Currently, `SettingsScreenMsg` only has one variant, so the if is ambiguous. + // + // if let SettingsScreenMsg::DaemonStateChange = msg { + self.diag_group.emit(DiagGroupMsg::Refresh); + self.daemon_group.emit(DaemonGroupMsg::DaemonStateChange); + // } + } } diff --git a/burrow-gtk/src/diag.rs b/burrow-gtk/src/diag.rs index 348293e..ab4757e 100644 --- a/burrow-gtk/src/diag.rs +++ b/burrow-gtk/src/diag.rs @@ -15,15 +15,18 @@ pub enum StatusTernary { // Realistically, we may not explicitly "support" non-systemd platforms which would simply this // code greatly. // Along with replacing [`StatusTernary`] with good old [`bool`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemSetup { Systemd, + AppImage, Other, } impl SystemSetup { pub fn new() -> Self { - if Command::new("systemctl").arg("--version").output().is_ok() { + if is_appimage() { + SystemSetup::AppImage + } else if Command::new("systemctl").arg("--version").output().is_ok() { SystemSetup::Systemd } else { SystemSetup::Other @@ -33,6 +36,7 @@ impl SystemSetup { pub fn is_service_installed(&self) -> Result { match self { SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SERVICE_LOC).is_ok().into()), + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -40,6 +44,7 @@ impl SystemSetup { pub fn is_socket_installed(&self) -> Result { match self { SystemSetup::Systemd => Ok(fs::metadata(SYSTEMD_SOCKET_LOC).is_ok().into()), + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -55,6 +60,7 @@ impl SystemSetup { let output = String::from_utf8(output)?; Ok((output == "enabled\n").into()) } + SystemSetup::AppImage => Ok(StatusTernary::NA), SystemSetup::Other => Ok(StatusTernary::NA), } } @@ -74,7 +80,12 @@ impl Display for SystemSetup { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { SystemSetup::Systemd => "Systemd", + SystemSetup::AppImage => "AppImage", SystemSetup::Other => "Other", }) } } + +pub fn is_appimage() -> bool { + std::env::vars().any(|(k, _)| k == "APPDIR") +} From 51fd638b7250f81b3899113033e2668d6975847c Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 24 Feb 2024 09:51:38 -0800 Subject: [PATCH 07/25] Update for Xcode 15.2 --- Apple/Burrow.xcodeproj/project.pbxproj | 2 +- Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme | 2 +- .../xcshareddata/xcschemes/NetworkExtension.xcscheme | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 428d9ab..6127e1a 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -319,7 +319,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1510; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1520; TargetAttributes = { D00117372B30341C00D87C25 = { CreatedOnToolsVersion = 15.1; diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index c63f8e6..670823d 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -1,6 +1,6 @@ Date: Sat, 24 Feb 2024 09:49:07 -0800 Subject: [PATCH 08/25] Introduce initial UI for connecting to networks --- .swiftlint.yml | 5 +- Apple/App/App.xcconfig | 5 + Apple/App/AppDelegate.swift | 3 +- .../HackClub.colorset/Contents.json | 20 + .../HackClub.imageset/Contents.json | 12 + .../flag-standalone-wtransparent.pdf | Bin 0 -> 3501 bytes .../WireGuard.colorset/Contents.json | 20 + .../WireGuard.imageset/Contents.json | 15 + .../WireGuard.imageset/WireGuard.svg | 6 + .../WireGuardTitle.imageset/Contents.json | 21 + .../WireGuardTitle.svg | 3 + Apple/App/BurrowApp.swift | 16 +- Apple/App/BurrowView.swift | 26 + Apple/App/FloatingButtonStyle.swift | 50 ++ Apple/App/MainMenu.xib | 679 ++++++++++++++++++ Apple/App/Menu/MenuView.swift | 60 -- Apple/App/MenuItemToggleView.swift | 64 ++ Apple/App/NetworkExtensionTunnel.swift | 167 +++++ Apple/App/NetworkView.swift | 88 +++ Apple/App/Networks/HackClub.swift | 23 + Apple/App/Networks/Network.swift | 10 + Apple/App/Networks/WireGuard.swift | 30 + Apple/App/Status.swift | 42 -- Apple/App/Tunnel.swift | 178 ++--- Apple/App/TunnelButton.swift | 61 ++ Apple/App/TunnelStatusView.swift | 37 + Apple/App/TunnelView.swift | 34 - Apple/Burrow.xcodeproj/project.pbxproj | 76 +- .../xcshareddata/swiftpm/Package.resolved | 16 +- .../PacketTunnelProvider.swift | 8 +- Apple/Shared/Constants.swift | 1 + Apple/Shared/Constants/Constants.h | 1 + Apple/Shared/Shared.xcconfig | 2 +- 33 files changed, 1458 insertions(+), 321 deletions(-) create mode 100644 Apple/App/Assets.xcassets/HackClub.colorset/Contents.json create mode 100644 Apple/App/Assets.xcassets/HackClub.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf create mode 100644 Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg create mode 100644 Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json create mode 100644 Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg create mode 100644 Apple/App/BurrowView.swift create mode 100644 Apple/App/FloatingButtonStyle.swift create mode 100644 Apple/App/MainMenu.xib delete mode 100644 Apple/App/Menu/MenuView.swift create mode 100644 Apple/App/MenuItemToggleView.swift create mode 100644 Apple/App/NetworkExtensionTunnel.swift create mode 100644 Apple/App/NetworkView.swift create mode 100644 Apple/App/Networks/HackClub.swift create mode 100644 Apple/App/Networks/Network.swift create mode 100644 Apple/App/Networks/WireGuard.swift delete mode 100644 Apple/App/Status.swift create mode 100644 Apple/App/TunnelButton.swift create mode 100644 Apple/App/TunnelStatusView.swift delete mode 100644 Apple/App/TunnelView.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index d609718..22ef035 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -46,7 +46,6 @@ opt_in_rules: - multiline_parameters - multiline_parameters_brackets - no_extension_access_modifier -- no_grouping_extension - nslocalizedstring_key - nslocalizedstring_require_bundle - number_separator @@ -76,9 +75,7 @@ opt_in_rules: - sorted_first_last - sorted_imports - static_operator -- strict_fileprivate - strong_iboutlet -- switch_case_on_newline - test_case_accessibility - toggle_bool - trailing_closure @@ -97,3 +94,5 @@ disabled_rules: - force_try - nesting - todo +- trailing_comma +- switch_case_on_newline diff --git a/Apple/App/App.xcconfig b/Apple/App/App.xcconfig index 1d63205..4e42ddc 100644 --- a/Apple/App/App.xcconfig +++ b/Apple/App/App.xcconfig @@ -11,7 +11,12 @@ INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 +EXCLUDED_SOURCE_FILE_NAMES = MainMenu.xib +EXCLUDED_SOURCE_FILE_NAMES[sdk=macosx*] = +INFOPLIST_KEY_LSUIElement[sdk=macosx*] = YES +INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu +INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index f42b52f..6085d85 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -3,6 +3,7 @@ import AppKit import SwiftUI @MainActor +@NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( @@ -16,7 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let toggleItem: NSMenuItem = { - let toggleView = NSHostingView(rootView: MenuItemToggleView(tunnel: BurrowApp.tunnel)) + let toggleView = NSHostingView(rootView: MenuItemToggleView()) toggleView.frame.size = CGSize(width: 300, height: 32) toggleView.autoresizingMask = [.width] diff --git a/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json b/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json new file mode 100644 index 0000000..911b4b1 --- /dev/null +++ b/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x37", + "red" : "0xEC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json b/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json new file mode 100644 index 0000000..ddd0664 --- /dev/null +++ b/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flag-standalone-wtransparent.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf b/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1506fe9390e19160cb5f8732ffe6ba2c488975ce GIT binary patch literal 3501 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 + + + + + \ No newline at end of file diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json new file mode 100644 index 0000000..782dd12 --- /dev/null +++ b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "WireGuardTitle.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg new file mode 100644 index 0000000..64946da --- /dev/null +++ b/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg @@ -0,0 +1,3 @@ + + + diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index e8aed86..21ebf84 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,21 +1,13 @@ import SwiftUI -@main +#if !os(macOS) @MainActor +@main struct BurrowApp: App { - static let tunnel = Tunnel { manager, proto in - proto.serverAddress = "hackclub.com" - manager.localizedDescription = "Burrow" - } - - #if os(macOS) - @NSApplicationDelegateAdaptor(AppDelegate.self) - var delegate - #endif - var body: some Scene { WindowGroup { - TunnelView(tunnel: Self.tunnel) + BurrowView() } } } +#endif diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift new file mode 100644 index 0000000..b78b1e1 --- /dev/null +++ b/Apple/App/BurrowView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct BurrowView: View { + var body: some View { + NavigationStack { + VStack { + NetworkCarouselView() + Spacer() + TunnelStatusView() + TunnelButton() + .padding(.bottom) + } + .padding() + .navigationTitle("Networks") + } + } +} + +#if DEBUG +struct NetworkView_Previews: PreviewProvider { + static var previews: some View { + BurrowView() + .environment(\.tunnel, PreviewTunnel()) + } +} +#endif diff --git a/Apple/App/FloatingButtonStyle.swift b/Apple/App/FloatingButtonStyle.swift new file mode 100644 index 0000000..53ab5ed --- /dev/null +++ b/Apple/App/FloatingButtonStyle.swift @@ -0,0 +1,50 @@ +import SwiftUI + +struct FloatingButtonStyle: ButtonStyle { + static let duration = 0.08 + + var color: Color + var cornerRadius: CGFloat + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.white) + .frame(minHeight: 48) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill( + LinearGradient( + colors: [ + configuration.isPressed ? color.opacity(0.9) : color.opacity(0.9), + configuration.isPressed ? color.opacity(0.9) : color + ], + startPoint: .init(x: 0.2, y: 0), + endPoint: .init(x: 0.8, y: 1) + ) + ) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(configuration.isPressed ? .black : .white) + ) + ) + .shadow(color: .black.opacity(configuration.isPressed ? 0.0 : 0.1), radius: 2.5, x: 0, y: 2) + .scaleEffect(configuration.isPressed ? 0.975 : 1.0) + .padding(.bottom, 2) + .animation( + configuration.isPressed ? .easeOut(duration: Self.duration) : .easeIn(duration: Self.duration), + value: configuration.isPressed + ) + } +} + +extension ButtonStyle where Self == FloatingButtonStyle { + static var floating: FloatingButtonStyle { + floating() + } + + static func floating(color: Color = .accentColor, cornerRadius: CGFloat = 10) -> FloatingButtonStyle { + FloatingButtonStyle(color: color, cornerRadius: cornerRadius) + } +} diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib new file mode 100644 index 0000000..8933f30 --- /dev/null +++ b/Apple/App/MainMenu.xib @@ -0,0 +1,679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apple/App/Menu/MenuView.swift b/Apple/App/Menu/MenuView.swift deleted file mode 100644 index eab8da2..0000000 --- a/Apple/App/Menu/MenuView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// MenuView.swift -// App -// -// Created by Thomas Stubblefield on 5/13/23. -// - -import SwiftUI - -struct MenuItemToggleView: View { - var tunnel: Tunnel - - var body: some View { - HStack { - Text("Burrow") - .font(.headline) - Spacer() - Toggle("Burrow", isOn: tunnel.isOn) - .labelsHidden() - .disabled(tunnel.isDisabled) - .toggleStyle(.switch) - } - .padding(.horizontal, 4) - .padding(10) - .frame(minWidth: 300, minHeight: 32, maxHeight: 32) - } -} - -extension Tunnel { - var isDisabled: Bool { - switch self.status { - case .disconnected, .permissionRequired, .connected: - return false - case .unknown, .disabled, .connecting, .reasserting, .disconnecting, .invalid, .configurationReadWriteFailed: - return true - } - } - - var isOn: Binding { - Binding { - switch self.status { - case .connecting, .reasserting, .connected: - true - default: - false - } - } set: { newValue in - switch (self.status, newValue) { - case (.permissionRequired, true): - Task { try await self.configure() } - case (.disconnected, true): - try? self.start() - case (.connected, false): - self.stop() - default: - return - } - } - } -} diff --git a/Apple/App/MenuItemToggleView.swift b/Apple/App/MenuItemToggleView.swift new file mode 100644 index 0000000..07db51d --- /dev/null +++ b/Apple/App/MenuItemToggleView.swift @@ -0,0 +1,64 @@ +// +// MenuItemToggleView.swift +// App +// +// Created by Thomas Stubblefield on 5/13/23. +// + +import SwiftUI + +struct MenuItemToggleView: View { + @Environment(\.tunnel) + var tunnel: Tunnel + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text("Burrow") + .font(.headline) + Text(tunnel.status.description) + .font(.subheadline) + } + Spacer() + Toggle(isOn: tunnel.toggleIsOn) { + } + .disabled(tunnel.toggleDisabled) + .toggleStyle(.switch) + } + .accessibilityElement(children: .combine) + .padding(.horizontal, 4) + .padding(10) + .frame(minWidth: 300, minHeight: 32, maxHeight: 32) + } +} + +extension Tunnel { + fileprivate var toggleDisabled: Bool { + switch status { + case .disconnected, .permissionRequired, .connected, .disconnecting: + false + case .unknown, .disabled, .connecting, .reasserting, .invalid, .configurationReadWriteFailed: + true + } + } + + var toggleIsOn: Binding { + Binding { + switch status { + case .connecting, .reasserting, .connected: + true + default: + false + } + } set: { newValue in + switch (status, newValue) { + case (.permissionRequired, true): + enable() + case (_, true): + start() + case (_, false): + stop() + } + } + } +} diff --git a/Apple/App/NetworkExtensionTunnel.swift b/Apple/App/NetworkExtensionTunnel.swift new file mode 100644 index 0000000..08002de --- /dev/null +++ b/Apple/App/NetworkExtensionTunnel.swift @@ -0,0 +1,167 @@ +import BurrowShared +import NetworkExtension + +@Observable +class NetworkExtensionTunnel: Tunnel { + @MainActor private(set) var status: TunnelStatus = .unknown + private var error: NEVPNError? + + private let logger = Logger.logger(for: Tunnel.self) + private let bundleIdentifier: String + private var tasks: [Task] = [] + + // Each manager corresponds to one entry in the Settings app. + // Our goal is to maintain a single manager, so we create one if none exist and delete any extra. + private var managers: [NEVPNManager]? { + didSet { Task { await updateStatus() } } + } + + private var currentStatus: TunnelStatus { + guard let managers = managers else { + guard let error = error else { + return .unknown + } + + switch error.code { + case .configurationReadWriteFailed: + return .configurationReadWriteFailed + default: + return .unknown + } + } + + guard let manager = managers.first else { + return .permissionRequired + } + + guard manager.isEnabled else { + return .disabled + } + + return manager.connection.tunnelStatus + } + + convenience init() { + self.init(Constants.networkExtensionBundleIdentifier) + } + + init(_ bundleIdentifier: String) { + self.bundleIdentifier = bundleIdentifier + + let center = NotificationCenter.default + let configurationChanged = Task { [weak self] in + for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { + await self?.update() + } + } + let statusChanged = Task { [weak self] in + for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { + await self?.updateStatus() + } + } + tasks = [configurationChanged, statusChanged] + + Task { await update() } + } + + private func update() async { + do { + managers = try await NETunnelProviderManager.managers + await self.updateStatus() + } catch let vpnError as NEVPNError { + error = vpnError + } catch { + logger.error("Failed to update VPN configurations: \(error)") + } + } + + private func updateStatus() async { + await MainActor.run { + status = currentStatus + } + } + + func configure() async throws { + if managers == nil { + await update() + } + + guard let managers = managers else { return } + + if managers.count > 1 { + try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in + for manager in managers.suffix(from: 1) { + group.addTask { try await manager.remove() } + } + try await group.waitForAll() + } + } + + guard managers.isEmpty else { return } + + let manager = NETunnelProviderManager() + manager.localizedDescription = "Burrow" + + let proto = NETunnelProviderProtocol() + proto.providerBundleIdentifier = bundleIdentifier + proto.serverAddress = "hackclub.com" + + manager.protocolConfiguration = proto + try await manager.save() + } + + func start() { + guard let manager = managers?.first else { return } + Task { + do { + if !manager.isEnabled { + manager.isEnabled = true + try await manager.save() + } + try manager.connection.startVPNTunnel() + } catch { + logger.error("Failed to start: \(error)") + } + } + } + + func stop() { + guard let manager = managers?.first else { return } + manager.connection.stopVPNTunnel() + } + + func enable() { + Task { + do { + try await configure() + } catch { + logger.error("Failed to enable: \(error)") + } + } + } + + deinit { + tasks.forEach { $0.cancel() } + } +} + +extension NEVPNConnection { + fileprivate var tunnelStatus: TunnelStatus { + switch status { + case .connected: + .connected(connectedDate!) + case .connecting: + .connecting + case .disconnecting: + .disconnecting + case .disconnected: + .disconnected + case .reasserting: + .reasserting + case .invalid: + .invalid + @unknown default: + .unknown + } + } +} diff --git a/Apple/App/NetworkView.swift b/Apple/App/NetworkView.swift new file mode 100644 index 0000000..290254c --- /dev/null +++ b/Apple/App/NetworkView.swift @@ -0,0 +1,88 @@ +import SwiftUI + +struct NetworkView: View { + var color: Color + var content: () -> Content + + private var gradient: LinearGradient { + LinearGradient( + colors: [ + color.opacity(0.8), + color + ], + startPoint: .init(x: 0.2, y: 0), + endPoint: .init(x: 0.8, y: 1) + ) + } + + var body: some View { + content() + .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(gradient) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + ) + .shadow(color: .black.opacity(0.1), radius: 3.0, x: 0, y: 2) + } +} + +struct AddNetworkView: View { + var body: some View { + Text("Add Network") + .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(style: .init(lineWidth: 2, dash: [6])) + ) + } +} + +extension NetworkView where Content == AnyView { + init(network: any Network) { + color = network.backgroundColor + content = { AnyView(network.label) } + } +} + +struct NetworkCarouselView: View { + var networks: [any Network] = [ + HackClub(id: "1"), + HackClub(id: "2"), + WireGuard(id: "4"), + HackClub(id: "5"), + ] + + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(networks, id: \.id) { network in + NetworkView(network: network) + .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) + .scrollTransition(.interactive, axis: .horizontal) { content, phase in + content + .scaleEffect(1.0 - abs(phase.value) * 0.1) + } + } + AddNetworkView() + } + .scrollTargetLayout() + } + .scrollClipDisabled() + .scrollIndicators(.hidden) + .defaultScrollAnchor(.center) + .scrollTargetBehavior(.viewAligned) + .containerRelativeFrame(.horizontal) + } +} + +#if DEBUG +struct NetworkCarouselView_Previews: PreviewProvider { + static var previews: some View { + NetworkCarouselView() + } +} +#endif diff --git a/Apple/App/Networks/HackClub.swift b/Apple/App/Networks/HackClub.swift new file mode 100644 index 0000000..f7df674 --- /dev/null +++ b/Apple/App/Networks/HackClub.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct HackClub: Network { + var id: String + var backgroundColor: Color { .init("HackClub") } + + var label: some View { + GeometryReader { reader in + VStack(alignment: .leading) { + Image("HackClub") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: reader.size.height / 4) + Spacer() + Text("@conradev") + .foregroundStyle(.white) + .font(.body.monospaced()) + } + .padding() + .frame(maxWidth: .infinity) + } + } +} diff --git a/Apple/App/Networks/Network.swift b/Apple/App/Networks/Network.swift new file mode 100644 index 0000000..d441d24 --- /dev/null +++ b/Apple/App/Networks/Network.swift @@ -0,0 +1,10 @@ +import SwiftUI + +protocol Network { + associatedtype Label: View + + var id: String { get } + var backgroundColor: Color { get } + + var label: Label { get } +} diff --git a/Apple/App/Networks/WireGuard.swift b/Apple/App/Networks/WireGuard.swift new file mode 100644 index 0000000..499288a --- /dev/null +++ b/Apple/App/Networks/WireGuard.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct WireGuard: Network { + var id: String + var backgroundColor: Color { .init("WireGuard") } + + var label: some View { + GeometryReader { reader in + VStack(alignment: .leading) { + HStack { + Image("WireGuard") + .resizable() + .aspectRatio(contentMode: .fit) + Image("WireGuardTitle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: reader.size.width / 2) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: reader.size.height / 4) + Spacer() + Text("@conradev") + .foregroundStyle(.white) + .font(.body.monospaced()) + } + .padding() + .frame(maxWidth: .infinity) + } + } +} diff --git a/Apple/App/Status.swift b/Apple/App/Status.swift deleted file mode 100644 index c08cdd1..0000000 --- a/Apple/App/Status.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import NetworkExtension - -extension Tunnel { - enum Status: CustomStringConvertible, Equatable, Hashable { - case unknown - case permissionRequired - case disabled - case connecting - case connected(Date) - case disconnecting - case disconnected - case reasserting - case invalid - case configurationReadWriteFailed - - var description: String { - switch self { - case .unknown: - return "Unknown" - case .permissionRequired: - return "Permission Required" - case .disconnected: - return "Disconnected" - case .disabled: - return "Disabled" - case .connecting: - return "Connecting" - case .connected: - return "Connected" - case .disconnecting: - return "Disconnecting" - case .reasserting: - return "Reasserting" - case .invalid: - return "Invalid" - case .configurationReadWriteFailed: - return "System Error" - } - } - } -} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift index 5542170..8db366f 100644 --- a/Apple/App/Tunnel.swift +++ b/Apple/App/Tunnel.swift @@ -1,146 +1,50 @@ -import BurrowShared -import NetworkExtension import SwiftUI +protocol Tunnel { + var status: TunnelStatus { get } + + func start() + func stop() + func enable() +} + +enum TunnelStatus: Equatable, Hashable { + case unknown + case permissionRequired + case disabled + case connecting + case connected(Date) + case disconnecting + case disconnected + case reasserting + case invalid + case configurationReadWriteFailed +} + +struct TunnelKey: EnvironmentKey { + static let defaultValue: any Tunnel = NetworkExtensionTunnel() +} + +extension EnvironmentValues { + var tunnel: any Tunnel { + get { self[TunnelKey.self] } + set { self[TunnelKey.self] = newValue } + } +} + +#if DEBUG @Observable -class Tunnel { - private(set) var status: Status = .unknown - private var error: NEVPNError? +class PreviewTunnel: Tunnel { + var status: TunnelStatus = .permissionRequired - private let logger = Logger.logger(for: Tunnel.self) - private let bundleIdentifier: String - private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void - private var tasks: [Task] = [] - - // Each manager corresponds to one entry in the Settings app. - // Our goal is to maintain a single manager, so we create one if none exist and delete extra if there are any. - private var managers: [NEVPNManager]? { - didSet { status = currentStatus } + func start() { + status = .connected(.now) } - - private var currentStatus: Status { - guard let managers = managers else { - guard let error = error else { - return .unknown - } - - switch error.code { - case .configurationReadWriteFailed: - return .configurationReadWriteFailed - default: - return .unknown - } - } - - guard let manager = managers.first else { - return .permissionRequired - } - - guard manager.isEnabled else { - return .disabled - } - - return manager.connection.tunnelStatus - } - - convenience init(configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { - self.init("com.hackclub.burrow.network", configure: configure) - } - - init(_ bundleIdentifier: String, configure: @escaping (NETunnelProviderManager, NETunnelProviderProtocol) -> Void) { - self.bundleIdentifier = bundleIdentifier - self.configure = configure - - let center = NotificationCenter.default - let configurationChanged = Task { - for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { - await update() - } - } - let statusChanged = Task { - for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { - await MainActor.run { - status = currentStatus - } - } - } - tasks = [configurationChanged, statusChanged] - - Task { await update() } - } - - private func update() async { - do { - let updated = try await NETunnelProviderManager.managers - await MainActor.run { - managers = updated - } - } catch let vpnError as NEVPNError { - error = vpnError - } catch { - logger.error("Failed to update VPN configurations: \(error)") - } - } - - func configure() async throws { - if managers == nil { - await update() - } - - guard let managers = managers else { return } - - if managers.count > 1 { - try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in - for manager in managers.suffix(from: 1) { - group.addTask { try await manager.remove() } - } - try await group.waitForAll() - } - } - - if managers.isEmpty { - let manager = NETunnelProviderManager() - let proto = NETunnelProviderProtocol() - proto.providerBundleIdentifier = bundleIdentifier - configure(manager, proto) - - manager.protocolConfiguration = proto - try await manager.save() - } - } - - func start() throws { - guard let manager = managers?.first else { return } - try manager.connection.startVPNTunnel() - } - func stop() { - guard let manager = managers?.first else { return } - manager.connection.stopVPNTunnel() + status = .disconnected } - - deinit { - tasks.forEach { $0.cancel() } - } -} - -extension NEVPNConnection { - var tunnelStatus: Tunnel.Status { - switch status { - case .connected: - .connected(connectedDate!) - case .connecting: - .connecting - case .disconnecting: - .disconnecting - case .disconnected: - .disconnected - case .reasserting: - .reasserting - case .invalid: - .invalid - @unknown default: - .unknown - } + func enable() { + status = .disconnected } } +#endif diff --git a/Apple/App/TunnelButton.swift b/Apple/App/TunnelButton.swift new file mode 100644 index 0000000..df8d7e6 --- /dev/null +++ b/Apple/App/TunnelButton.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct TunnelButton: View { + @Environment(\.tunnel) + var tunnel: any Tunnel + + var body: some View { + if let action = tunnel.action { + Button { + tunnel.perform(action) + } label: { + Text(action.description) + } + .padding(.horizontal) + .buttonStyle(.floating) + } + } +} + +extension Tunnel { + fileprivate var action: TunnelButton.Action? { + switch status { + case .permissionRequired, .invalid: + .enable + case .disabled, .disconnecting, .disconnected: + .start + case .connecting, .connected, .reasserting: + .stop + case .unknown, .configurationReadWriteFailed: + nil + } + } +} + +extension TunnelButton { + fileprivate enum Action { + case enable + case start + case stop + } +} + +extension TunnelButton.Action { + var description: LocalizedStringKey { + switch self { + case .enable: "Enable" + case .start: "Start" + case .stop: "Stop" + } + } +} + +extension Tunnel { + fileprivate func perform(_ action: TunnelButton.Action) { + switch action { + case .enable: enable() + case .start: start() + case .stop: stop() + } + } +} diff --git a/Apple/App/TunnelStatusView.swift b/Apple/App/TunnelStatusView.swift new file mode 100644 index 0000000..3593516 --- /dev/null +++ b/Apple/App/TunnelStatusView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct TunnelStatusView: View { + @Environment(\.tunnel) + var tunnel: any Tunnel + + var body: some View { + Text(tunnel.status.description) + } +} + +extension TunnelStatus: CustomStringConvertible { + var description: String { + switch self { + case .unknown: + "Unknown" + case .permissionRequired: + "Permission Required" + case .disconnected: + "Disconnected" + case .disabled: + "Disabled" + case .connecting: + "Connecting…" + case .connected: + "Connected" + case .disconnecting: + "Disconnecting…" + case .reasserting: + "Reasserting…" + case .invalid: + "Invalid" + case .configurationReadWriteFailed: + "System Error" + } + } +} diff --git a/Apple/App/TunnelView.swift b/Apple/App/TunnelView.swift deleted file mode 100644 index dd91603..0000000 --- a/Apple/App/TunnelView.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -struct TunnelView: View { - var tunnel: Tunnel - - var body: some View { - VStack { - Text(verbatim: tunnel.status.description) - switch tunnel.status { - case .connected: - Button("Disconnect", action: stop) - case .permissionRequired: - Button("Allow", action: configure) - case .disconnected: - Button("Start", action: start) - default: - EmptyView() - } - } - .padding() - } - - private func start() { - try? tunnel.start() - } - - private func stop() { - tunnel.stop() - } - - private func configure() { - Task { try await tunnel.configure() } - } -} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 6127e1a..8717a30 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -9,24 +9,32 @@ /* Begin PBXBuildFile section */ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; - 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuView.swift */; }; + 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; + D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A79302B81630D0024EC91 /* NetworkView.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6512B8A79C20006B8AD /* HackClub.swift */; }; + D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6532B8A79DA0006B8AD /* WireGuard.swift */; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; - D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */; }; + D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */; }; D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; + D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */; }; D08252762B5C9FC4005DA378 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08252752B5C9FC4005DA378 /* Constants.swift */; }; + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; }; D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; - D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FE2A086E1C00AD070D /* Status.swift */; }; D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; D0BCC60A2A09A0B800AD070D /* build-rust.sh in Resources */ = {isa = PBXBuildFile; fileRef = D0B98FBF29FD8072004E7149 /* build-rust.sh */; }; + D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */; }; + D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5952B818B2900F6A84B /* TunnelButton.swift */; }; + D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */; }; + D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5992B818B9600F6A84B /* Network.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -70,7 +78,7 @@ /* Begin PBXFileReference section */ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; - 43AA26D72A10004900F14CE6 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -78,6 +86,7 @@ D00117412B30347800D87C25 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D00117422B30348D00D87C25 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D01A79302B81630D0024EC91 /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -92,19 +101,26 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetworkExtension-iOS.entitlements"; sourceTree = ""; }; D020F66829E4AA74002790F6 /* App-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-iOS.entitlements"; sourceTree = ""; }; D020F66929E4AA74002790F6 /* App-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-macOS.entitlements"; sourceTree = ""; }; + D032E6512B8A79C20006B8AD /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; + D032E6532B8A79DA0006B8AD /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; D05B9F7229E39EEC008CB1F9 /* Burrow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Burrow.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; - D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelView.swift; sourceTree = ""; }; + D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; D08252742B5C9DEB005DA378 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; D08252752B5C9FC4005DA378 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + D09150412B9D2AF700BE3CB0 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; - D0BCC5FE2A086E1C00AD070D /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; + D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; + D0FAB5952B818B2900F6A84B /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; + D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; + D0FAB5992B818B9600F6A84B /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -135,14 +151,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 43AA26D62A0FFFD000F14CE6 /* Menu */ = { - isa = PBXGroup; - children = ( - 43AA26D72A10004900F14CE6 /* MenuView.swift */, - ); - path = Menu; - sourceTree = ""; - }; D00117392B30341C00D87C25 /* Shared */ = { isa = PBXGroup; children = ( @@ -199,6 +207,16 @@ path = NetworkExtension; sourceTree = ""; }; + D032E64D2B8A69C90006B8AD /* Networks */ = { + isa = PBXGroup; + children = ( + D0FAB5992B818B9600F6A84B /* Network.swift */, + D032E6512B8A79C20006B8AD /* HackClub.swift */, + D032E6532B8A79DA0006B8AD /* WireGuard.swift */, + ); + path = Networks; + sourceTree = ""; + }; D05B9F6929E39EEC008CB1F9 = { isa = PBXGroup; children = ( @@ -224,14 +242,20 @@ D05B9F7429E39EEC008CB1F9 /* App */ = { isa = PBXGroup; children = ( - 43AA26D62A0FFFD000F14CE6 /* Menu */, D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, D00AA8962A4669BC005C8102 /* AppDelegate.swift */, - D05B9F7729E39EEC008CB1F9 /* TunnelView.swift */, + 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, + D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, + D01A79302B81630D0024EC91 /* NetworkView.swift */, + D032E64D2B8A69C90006B8AD /* Networks */, + D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, + D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, - D0BCC5FE2A086E1C00AD070D /* Status.swift */, + D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */, D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */, + D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */, D05B9F7929E39EED008CB1F9 /* Assets.xcassets */, + D09150412B9D2AF700BE3CB0 /* MainMenu.xib */, D020F66829E4AA74002790F6 /* App-iOS.entitlements */, D020F66929E4AA74002790F6 /* App-macOS.entitlements */, D020F64929E4A34B002790F6 /* App.xcconfig */, @@ -369,6 +393,7 @@ buildActionMask = 2147483647; files = ( D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */, + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -423,12 +448,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */, D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, - 43AA26D82A10004900F14CE6 /* MenuView.swift in Sources */, - D05B9F7829E39EEC008CB1F9 /* TunnelView.swift in Sources */, - D0BCC5FF2A086E1C00AD070D /* Status.swift in Sources */, + D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */, + 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, + D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, + D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, + D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, + D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, + D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, + D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, + D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -568,8 +600,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.54.0; + branch = main; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7522840..9378372 100644 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint.git", "state" : { - "revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee", - "version" : "0.54.0" + "branch" : "main", + "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/drmohundro/SWXMLHash.git", "state" : { - "revision" : "4d0f62f561458cbe1f732171e625f03195151b60", - "version" : "7.0.1" + "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version" : "7.0.2" } }, { diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 7073401..a07daa3 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -6,10 +6,16 @@ import os class PacketTunnelProvider: NEPacketTunnelProvider { private let logger = Logger.logger(for: PacketTunnelProvider.self) - override func startTunnel(options: [String: NSObject]? = nil) async throws { + override init() { do { libburrow.spawnInProcess(socketPath: try Constants.socketURL.path) + } catch { + logger.error("Failed to spawn: \(error)") + } + } + override func startTunnel(options: [String: NSObject]? = nil) async throws { + do { let client = try Client() let command = BurrowRequest(id: 0, command: "ServerConfig") diff --git a/Apple/Shared/Constants.swift b/Apple/Shared/Constants.swift index cb56cb3..634c500 100644 --- a/Apple/Shared/Constants.swift +++ b/Apple/Shared/Constants.swift @@ -7,6 +7,7 @@ public enum Constants { public static let bundleIdentifier = AppBundleIdentifier public static let appGroupIdentifier = AppGroupIdentifier + public static let networkExtensionBundleIdentifier = NetworkExtensionBundleIdentifier public static var groupContainerURL: URL { get throws { try _groupContainerURL.get() } diff --git a/Apple/Shared/Constants/Constants.h b/Apple/Shared/Constants/Constants.h index 09806c5..5278b61 100644 --- a/Apple/Shared/Constants/Constants.h +++ b/Apple/Shared/Constants/Constants.h @@ -7,5 +7,6 @@ NS_ASSUME_NONNULL_BEGIN static NSString * const AppBundleIdentifier = MACRO_STRING(APP_BUNDLE_IDENTIFIER); static NSString * const AppGroupIdentifier = MACRO_STRING(APP_GROUP_IDENTIFIER); +static NSString * const NetworkExtensionBundleIdentifier = MACRO_STRING(NETWORK_EXTENSION_BUNDLE_IDENTIFIER); NS_ASSUME_NONNULL_END diff --git a/Apple/Shared/Shared.xcconfig b/Apple/Shared/Shared.xcconfig index 50718bd..f344e8b 100644 --- a/Apple/Shared/Shared.xcconfig +++ b/Apple/Shared/Shared.xcconfig @@ -2,4 +2,4 @@ PRODUCT_NAME = BurrowShared MERGEABLE_LIBRARY = YES SWIFT_INCLUDE_PATHS = $(PROJECT_DIR)/Shared/Constants -GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) +GCC_PREPROCESSOR_DEFINITIONS = APP_BUNDLE_IDENTIFIER=$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER=$(APP_GROUP_IDENTIFIER) NETWORK_EXTENSION_BUNDLE_IDENTIFIER=$(NETWORK_EXTENSION_BUNDLE_IDENTIFIER) From 4334f8c9c9e31f92ddfe94f5aaddc6e91432d16d Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 16 Mar 2024 10:34:59 -0700 Subject: [PATCH 09/25] Configure CARGO_TARGET_DIR to be inside of DerivedData --- .github/actions/build-for-testing/action.yml | 10 ++++++---- .github/actions/export/action.yml | 3 +++ Apple/NetworkExtension/libburrow/build-rust.sh | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index ce91b43..2c66963 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -24,6 +24,7 @@ runs: path: | Apple/PackageCache Apple/SourcePackages + Apple/DerivedData key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} restore-keys: | ${{ runner.os }}-${{ inputs.scheme }}- @@ -33,17 +34,18 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - xcodebuild clean build-for-testing \ + xcodebuild build-for-testing \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -onlyUsePackageVersionsFromResolvedFile \ -clonedSourcePackagesDirPath SourcePackages \ -packageCachePath $PWD/PackageCache \ - -skipPackagePluginValidation \ - -skipMacroValidation \ + -derivedDataPath $PWD/DerivedData \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -resultBundlePath BuildResults.xcresult diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index bf007a7..635732c 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -37,6 +37,9 @@ runs: -exportArchive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index 1ac73fb..fffa0d0 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -56,10 +56,10 @@ CARGO_ARGS+=("--lib") # Pass the configuration (Debug or Release) through to cargo if [[ $SWIFT_ACTIVE_COMPILATION_CONDITIONS == *DEBUG* ]]; then - CARGO_DIR="debug" + CARGO_TARGET_SUBDIR="debug" else CARGO_ARGS+=("--release") - CARGO_DIR="release" + CARGO_TARGET_SUBDIR="release" fi if [[ -x "$(command -v rustup)" ]]; then @@ -70,11 +70,11 @@ fi # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" # Use `lipo` to merge the architectures together into BUILT_PRODUCTS_DIR /usr/bin/xcrun --sdk $PLATFORM_NAME lipo \ - -create $(printf "${PROJECT_DIR}/../target/%q/${CARGO_DIR}/libburrow.a " "${RUST_TARGETS[@]}") \ + -create $(printf "${CONFIGURATION_TEMP_DIR}/target/%q/${CARGO_TARGET_SUBDIR}/libburrow.a " "${RUST_TARGETS[@]}") \ -output "${BUILT_PRODUCTS_DIR}/libburrow.a" From cb1bc1c8aa01e25ed0c7ddc65812d6670288a16b Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 16 Mar 2024 10:40:09 -0700 Subject: [PATCH 10/25] Update release pipelines to upload release artifacts --- .github/actions/archive/action.yml | 7 +- .github/actions/build-for-testing/action.yml | 2 +- .github/actions/export/action.yml | 5 +- .github/actions/notarize/action.yml | 58 ++++ .../actions/test-without-building/action.yml | 7 +- .github/workflows/build-appimage.yml | 7 +- .github/workflows/build-apple.yml | 9 +- .github/workflows/build-docker.yml | 1 + .github/workflows/build-rpm.yml | 8 +- .github/workflows/build-rust.yml | 2 +- .github/workflows/lint-git.yml | 21 +- .github/workflows/lint-swift.yml | 7 +- .github/workflows/release-appimage.yml | 29 ++ .github/workflows/release-apple.yml | 102 +++++-- .github/workflows/release-if-needed.yaml | 21 ++ .github/workflows/release-now.yml | 17 ++ .../AppIcon.appiconset/100.png | Bin 0 -> 4300 bytes .../AppIcon.appiconset/1024.png | Bin 0 -> 97634 bytes .../AppIcon.appiconset/114.png | Bin 0 -> 4903 bytes .../AppIcon.appiconset/120.png | Bin 0 -> 5372 bytes .../AppIcon.appiconset/128.png | Bin 0 -> 5713 bytes .../AppIcon.appiconset/144.png | Bin 0 -> 6559 bytes .../AppIcon.appiconset/152.png | Bin 0 -> 7005 bytes .../Assets.xcassets/AppIcon.appiconset/16.png | Bin 0 -> 684 bytes .../AppIcon.appiconset/167.png | Bin 0 -> 7880 bytes .../AppIcon.appiconset/172.png | Bin 0 -> 7994 bytes .../AppIcon.appiconset/180.png | Bin 0 -> 8614 bytes .../AppIcon.appiconset/196.png | Bin 0 -> 9835 bytes .../Assets.xcassets/AppIcon.appiconset/20.png | Bin 0 -> 927 bytes .../AppIcon.appiconset/216.png | Bin 0 -> 10925 bytes .../AppIcon.appiconset/256.png | Bin 0 -> 12031 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 0 -> 1308 bytes .../Assets.xcassets/AppIcon.appiconset/32.png | Bin 0 -> 1460 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 0 -> 1732 bytes .../Assets.xcassets/AppIcon.appiconset/48.png | Bin 0 -> 2060 bytes .../Assets.xcassets/AppIcon.appiconset/50.png | Bin 0 -> 2130 bytes .../AppIcon.appiconset/512.png | Bin 0 -> 30526 bytes .../Assets.xcassets/AppIcon.appiconset/55.png | Bin 0 -> 2298 bytes .../Assets.xcassets/AppIcon.appiconset/57.png | Bin 0 -> 2470 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 0 -> 2497 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 0 -> 2536 bytes .../Assets.xcassets/AppIcon.appiconset/64.png | Bin 0 -> 2614 bytes .../Assets.xcassets/AppIcon.appiconset/72.png | Bin 0 -> 2949 bytes .../Assets.xcassets/AppIcon.appiconset/76.png | Bin 0 -> 3340 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 0 -> 3427 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 0 -> 3704 bytes .../Assets.xcassets/AppIcon.appiconset/88.png | Bin 0 -> 3738 bytes .../AppIcon.appiconset/Contents.json | 285 +++++++++++++++++- Apple/Burrow.xcodeproj/project.pbxproj | 26 +- Apple/Configuration/Compiler.xcconfig | 2 +- Apple/Configuration/Version.xcconfig | 0 Tools/version.sh | 51 ++++ 52 files changed, 593 insertions(+), 74 deletions(-) create mode 100644 .github/actions/notarize/action.yml create mode 100644 .github/workflows/release-appimage.yml create mode 100644 .github/workflows/release-if-needed.yaml create mode 100644 .github/workflows/release-now.yml create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/100.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/114.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/120.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/128.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/144.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/152.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/16.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/167.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/172.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/180.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/196.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/20.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/216.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/256.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/29.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/32.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/40.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/48.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/50.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/512.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/55.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/57.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/58.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/60.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/64.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/72.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/76.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/80.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/87.png create mode 100644 Apple/App/Assets.xcassets/AppIcon.appiconset/88.png create mode 100644 Apple/Configuration/Version.xcconfig create mode 100755 Tools/version.sh diff --git a/.github/actions/archive/action.yml b/.github/actions/archive/action.yml index c34bd3c..37282e1 100644 --- a/.github/actions/archive/action.yml +++ b/.github/actions/archive/action.yml @@ -26,9 +26,12 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - xcodebuild archive \ + xcodebuild clean archive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ @@ -38,6 +41,4 @@ runs: -archivePath '${{ inputs.archive-path }}' \ -resultBundlePath BuildResults.xcresult - ./Tools/xcresulttool-github BuildResults.xcresult - rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 2c66963..084ba81 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -18,7 +18,7 @@ inputs: runs: using: composite steps: - - name: Cache Swift Packages + - name: Xcode Cache uses: actions/cache@v3 with: path: | diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index 635732c..8f891be 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -1,4 +1,4 @@ -name: Notarize +name: Export inputs: app-store-key: description: App Store key in PEM PKCS#8 format @@ -24,8 +24,7 @@ inputs: runs: using: composite steps: - - id: notarize - shell: bash + - shell: bash working-directory: Apple run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml new file mode 100644 index 0000000..290ed86 --- /dev/null +++ b/.github/actions/notarize/action.yml @@ -0,0 +1,58 @@ +name: Notarize +inputs: + app-store-key: + description: App Store key in PEM PKCS#8 format + required: true + app-store-key-id: + description: App Store key ID + required: true + app-store-key-issuer-id: + description: App Store key issuer ID + required: true + archive-path: + description: Xcode archive path + required: true + export-path: + description: The path to export the archive to + required: true +outputs: + notarized-app: + description: The compressed and notarized app + value: ${{ steps.notarize.outputs.notarized-app }} +runs: + using: composite + steps: + - id: notarize + shell: bash + working-directory: Apple + run: | + echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 + + echo '{"destination":"upload","method":"developer-id"}' \ + | plutil -convert xml1 -o ExportOptions.plist - + + xcodebuild \ + -exportArchive \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration \ + -authenticationKeyID ${{ inputs.app-store-key-id }} \ + -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ + -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ + -archivePath '${{ inputs.archive-path }}' \ + -exportOptionsPlist ExportOptions.plist + + until xcodebuild \ + -exportNotarizedApp \ + -allowProvisioningUpdates \ + -allowProvisioningDeviceRegistration \ + -authenticationKeyID ${{ inputs.app-store-key-id }} \ + -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ + -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ + -archivePath '${{ inputs.archive-path }}' \ + -exportPath ${{ inputs.export-path }} + do + echo "Failed to export app, trying again in 10s..." + sleep 10 + done + + rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist diff --git a/.github/actions/test-without-building/action.yml b/.github/actions/test-without-building/action.yml index 5903d07..a097d4a 100644 --- a/.github/actions/test-without-building/action.yml +++ b/.github/actions/test-without-building/action.yml @@ -18,9 +18,6 @@ inputs: runs: using: composite steps: - - shell: bash - id: vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - shell: bash working-directory: Apple run: | @@ -28,10 +25,10 @@ runs: -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ ${{ inputs.test-plan && '-testPlan ' }}${{ inputs.test-plan }} \ - -resultBundlePath "${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult" + -resultBundlePath "${{ inputs.artifact-prefix }}.xcresult" - uses: kishikawakatsumi/xcresulttool@v1 if: always() with: - path: Apple/${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult + path: Apple/${{ inputs.artifact-prefix }}.xcresult title: ${{ inputs.check-name }} show-passed-tests: false diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index ef5c525..bb510fb 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -1,8 +1,11 @@ name: Build AppImage on: push: - branches: [main] + branches: + - main pull_request: + branches: + - "*" jobs: appimage: name: Build AppImage @@ -17,7 +20,7 @@ jobs: docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . docker rm temp - uses: actions/upload-artifact@v4 + name: Upload to GitHub with: name: AppImage path: Burrow-x86_64.AppImage - diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index da0f56a..00b6bec 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -1,4 +1,4 @@ -name: Apple Build +name: Build Apple Apps on: push: branches: @@ -12,7 +12,7 @@ concurrency: jobs: build: name: Build App (${{ matrix.platform }}) - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -53,7 +53,6 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: stable targets: ${{ join(matrix.rust-targets, ', ') }} - name: Build id: build @@ -64,7 +63,7 @@ jobs: app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - - name: Xcode Unit Test + - name: Run Unit Tests if: ${{ matrix.xcode-unit-test != '' }} continue-on-error: true uses: ./.github/actions/test-without-building @@ -74,7 +73,7 @@ jobs: test-plan: ${{ matrix.xcode-unit-test }} artifact-prefix: unit-tests-${{ matrix.sdk-name }} check-name: Xcode Unit Tests (${{ matrix.platform }}) - - name: Xcode UI Test + - name: Run UI Tests if: ${{ matrix.xcode-ui-test != '' }} continue-on-error: true uses: ./.github/actions/test-without-building diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 1ce7a9a..307a93c 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -33,6 +33,7 @@ jobs: images: ghcr.io/${{ github.repository }} tags: | type=sha + type=match,pattern=builds/(.*),group=1 type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push uses: docker/build-push-action@v4 diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index fd5837c..e0ce8df 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -1,10 +1,5 @@ +on: workflow_dispatch name: Build RPM -on: - push: - branches: [ "main" ] - pull_request: - branches: - - "*" jobs: build: name: Build RPM @@ -20,4 +15,3 @@ jobs: strip -s target/release/burrow - name: Build RPM run: cargo generate-rpm -p burrow - diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 4c3782a..3255fc7 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -1,4 +1,4 @@ -name: Rust Build +name: Build Rust Crate on: push: branches: diff --git a/.github/workflows/lint-git.yml b/.github/workflows/lint-git.yml index aefe199..2f7c72e 100644 --- a/.github/workflows/lint-git.yml +++ b/.github/workflows/lint-git.yml @@ -8,13 +8,14 @@ jobs: name: Git Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - name: Install Gitlint - shell: bash - run: python -m pip install gitlint - - name: Run Gitlint - shell: bash - run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD" + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Install + shell: bash + run: python -m pip install gitlint + - name: Lint + shell: bash + run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD" diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml index 7e62afd..a2cc96a 100644 --- a/.github/workflows/lint-swift.yml +++ b/.github/workflows/lint-swift.yml @@ -1,8 +1,5 @@ name: Swift Lint on: - push: - branches: - - main pull_request: branches: - "*" @@ -14,8 +11,6 @@ jobs: image: ghcr.io/realm/swiftlint:latest steps: - name: Checkout - uses: actions/checkout@v3 - with: - ssh-key: ${{ secrets.DEPLOY_KEY }} + uses: actions/checkout@v4 - name: Lint run: swiftlint lint --reporter github-actions-logging diff --git a/.github/workflows/release-appimage.yml b/.github/workflows/release-appimage.yml new file mode 100644 index 0000000..e566186 --- /dev/null +++ b/.github/workflows/release-appimage.yml @@ -0,0 +1,29 @@ +name: Release (AppImage) +on: + release: + types: + - created +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Upload to GitHub + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow-x86_64.AppImage diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 3ea185d..786fb54 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -1,65 +1,115 @@ -name: Build Apple Release +name: Release (Apple) on: release: types: - created jobs: build: - name: Build ${{ matrix.configuration['platform'] }} Release - runs-on: macos-13 + name: Build ${{ matrix.platform }} Release + runs-on: macos-14 + permissions: + contents: write strategy: fail-fast: false matrix: - configuration: - - scheme: App (iOS) - destination: generic/platform=iOS + include: + - destination: generic/platform=iOS platform: iOS - method: ad-hoc - artifact-file: Apple/Release/Burrow.ipa - - scheme: App (macOS) - destination: generic/platform=macOS + rust-targets: + - aarch64-apple-ios + - destination: generic/platform=macOS platform: macOS - method: mac-application - artifact-file: Burrow.app.txz + rust-targets: + - x86_64-apple-darwin + - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - ssh-key: ${{ secrets.DEPLOY_KEY }} - submodules: recursive + fetch-depth: 0 - name: Import Certificate uses: ./.github/actions/import-cert with: certificate: ${{ secrets.DEVELOPER_CERT }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Configure Version + shell: bash + run: Tools/version.sh - name: Archive uses: ./.github/actions/archive with: - scheme: ${{ matrix.configuration['scheme'] }} - destination: ${{ matrix.configuration['destination'] }} + scheme: App + destination: ${{ matrix.destination }} app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive - - name: Export Locally + - name: Notarize (macOS) + if: ${{ matrix.platform == 'macOS' }} + uses: ./.github/actions/notarize + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + archive-path: Burrow.xcarchive + - name: Export IPA (iOS) + if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: - method: ${{ matrix.configuration['method'] }} + method: ad-hoc destination: export app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive export-path: Release - - name: Compress - if: ${{ matrix.configuration['platform'] == 'macOS' }} + - name: Compress (iOS) + if: ${{ matrix.platform == 'iOS' }} shell: bash - run: tar --options xz:compression-level=9 -C Apple/Release -cJf Burrow.app.txz ./ - - name: Attach Artifact - uses: SierraSoftworks/gh-releases@v1.0.6 + run: | + cp Apple/Release/Burrow.ipa Burrow.ipa + aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar + rm -rf Apple/Release + - name: Compress (macOS) + if: ${{ matrix.platform == 'macOS' }} + shell: bash + run: | + aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar + aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar + rm -rf Apple/Release + - name: Upload to GitHub (iOS) + if: ${{ matrix.platform == 'iOS' }} + uses: SierraSoftworks/gh-releases@v1.0.7 with: token: ${{ secrets.GITHUB_TOKEN }} - overwrite: 'false' - files: ${{ matrix.configuration['artifact-file'] }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow.ipa + Burrow-${{ matrix.platform }}.xcarchive.aar + - name: Upload to GitHub (macOS) + if: ${{ matrix.platform == 'macOS' }} + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: 'true' + files: | + Burrow.aap.aar + Burrow-${{ matrix.platform }}.xcarchive.aar + - name: Upload to App Store Connect + uses: ./.github/actions/export + with: + method: app-store + destination: upload + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + archive-path: Burrow.xcarchive + export-path: Release diff --git a/.github/workflows/release-if-needed.yaml b/.github/workflows/release-if-needed.yaml new file mode 100644 index 0000000..0d2eb97 --- /dev/null +++ b/.github/workflows/release-if-needed.yaml @@ -0,0 +1,21 @@ +name: Create Release If Needed +on: + workflow_dispatch: + schedule: + - cron: '0 10 * * *' +concurrency: + group: ${{ github.workflow }} +jobs: + create: + name: Create Release If Needed + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - shell: bash + run: | + if [[ $(Tools/version.sh status) == "dirty" ]]; then + gh workflow run release-now.yml + fi diff --git a/.github/workflows/release-now.yml b/.github/workflows/release-now.yml new file mode 100644 index 0000000..229f6c9 --- /dev/null +++ b/.github/workflows/release-now.yml @@ -0,0 +1,17 @@ +name: Create Release +on: workflow_dispatch +concurrency: + group: ${{ github.workflow }} +jobs: + create: + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - shell: bash + run: Tools/version.sh increment diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000000000000000000000000000000000..f86c1399c3d698390d66fbb50d647227b18e27d3 GIT binary patch literal 4300 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..872c9ceebe2427014f0b0df5ac04e039eab233e7 GIT binary patch literal 97634 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 Avj6}9 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb278dcb871e79ea334cbf2fb52709ab2f59358 GIT binary patch literal 4903 zcmeAS@N?(olHy`uVBq!ia0y~yU?>7%4mJh`hW@*)wG0dljKx9jP7LeL$-HD>U~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0p{VpiyFOqpzu)~|<-MfN_g7yb2)^}_j#LCaJmRtTQ;HWw{ zw_u0(*U(Sl_pbE4d2{ZJ-*-RNy;q~}2LHYH|9W+3Y^>3VpA$-*-U&)*f0rr|;ovCg z(D875A#hW1lLCi`OK+cRS%(e>6Ki1doTe9ni`eo*zFj!F@tBmhv`I#Q#gwoqI|5vs z?(P5oZ+5ToIg8Mfzn;&pH}ZI@;U)28>iODs2CYBe@7LeFk=!p^@$i57y-M}!$Gzs8 z;_LsKF6p)Z_v0oLw~jzx;c;2>C62q+n7nU!HqYaPKx2H}&(v#IuTH&NbXxc3?EHO_ zYm8Mh?o~cNYj!hba?bSFvXgB*l0h;~8&BAh{r!I)`B}d;F~3`K`C;s% zhn!{i&bBodSLbdzsaEVRSGnZ(zu)f{|9Ud>y2E0H@Pp9{HaEoA|NWY`@8`2_6~{Ro zD&Kl%7u+y;C%AI+S+m)@zg+UZd%ym_Y=J_3-=5Yx0-awua~$gad^~=4)#`OpoPwE0 zlvW0dgh$dk9~*4`-A`!d zy(1bP^U$qN=H?nJa?st=`uk-xPjfHK^IwJOW;!`i6r-|toL_P76Q^6%Cu zA>rFEmgshF`FbV3{_j)0T`!c(PwQ-+(xBfx>9>qk$%@^--|bf3@}yC_jiLHQo4b7N zm9=|5oeKRtr|eed-472B-#u^tU#4za75mexe1*}1oIkcbaX#~?TVL+rGCyz5IhD_5 z-enb!dGPPg=kpEAby=P=_^ppGK5Kez|D+@7EBdGil8v%5!z zhDVq0zme3v)B4>G36YxnGv98zk<={|5W+7MY~E{ruOe@L?YF}D zwcjE|u19(KlC z$KG|9uN5iYTzTXO+dPw2kJq#Q{dmk@ey{TRPRaB+8<(g!FM0bqrt0NV+0sc))&d%l z9UHZ7U;q8(vj6U@;qiNC`)v#GGdwEtu0`0dAo*BNXQXNOg*GA1#;^;w^LC%r-E=}J zbn4vNZ#VBgogOb&pU&-gWnZsfkZk_muVLFSIB|zwEj(xWyd*rXGBsy`sq^tRftk~; zrO&Uuwsz;US)o%GZ;>m%Q`oo5{h%1r(yiG%lB<5#{r_8D{%+^}bXwPYL%?A^p99V3GudSd8h-!ze17q<+JlS2U2d_L zm6lrCe_QI3(Xy3yW~+GIjlg)V1zWfpFLenlZ?XGw!8xy0H0(uN@78lKm(RZk3L(M% zdyj(5Cvu#2+?%lf_0-p2XJjs$>C`=Snl__i?nzyy%BLj`{(?+lU*gW1UXQuUv9(n; z|LfiI`>k(&`0ibpr==9jcaf{>HnrX+Wi0Xx$#8RnoY-Dx;m!ERcY>G=3H(F(T#J3KWJ2wE+wU%>Cl2p=yiGMoC_2$|_s3(>bLSh%aO#GaUs!md@4iOv z!bRL~=M;Fxs^}63re2}=BPqb^BdQeN0z=}KPtzPSxALcQ3aY|iyjhFF| z>hzdLGrl`|I0oE1+jsf-y^_nmrrgTSn>Bj2s$F>2=h3Kp@aKH~4eYWd0Vf@F1hei6 z6^p&!^;$1gFsgl9$Z>97JI~h#=Y{Z}@ON&TUe6lcF?Cr)#gk)7suqR}H}$mb4&3rj zyeh@TBOkbeZNj^kzH@|b`1~yLU2?#eak22uux5@)l1iWMRlnDqov`r9)eeP(liqU5 zvYP_eDPE55U-jzHKJ^8Tjm!Y zlQex@a7}i{k4N2$%ROIT`Xc%C-}HN}3PD^^2AQIloI_SS9ytx3> zdMtP9M5Rd*3v!z~Dm%70vRq)89mTzP-~5_SCudCE`l;xrkxb#|v*!Ahyh4@NrY_GG zTJrN4`?boT3Z+)}EX#h|ZxLtN1g{-diP*(1%HXqL;xQ)v-*30i|Li@BRVY;9lxfPF zSu9U3o(Pve84}Sg!q|0rSC5?1Gw)TW&wmqeWP4e1!_&n%=xRP z|Kxjfra$V$pC>mbyo)W2*mXHNKVtLL-W`3Cqd-TKNXOF+|9@vbx;eM} z-prqlem9Ly>j{_P3e%Q&idbTI$nXwl(Tb&#F_5 z>~#!pXDsezy7(tdY<10N3GpP&oI9$FMx1piPS4bjr~JEXv?FBQi2CGI8%$15))Qrb^6DFg_ZT8Y70?QIb%vRCI`ux62Jjdkw$(k+Jwdxd!(WBtdz>A;!w(IQ*6 z{4nfUdhbHA(pQE3AH&&Zvb1?lsQdr-dwQMtY=gdI{X*-*+ZfC37mNO}s+%dFJ^N>1 z38V9dCkH&(_~m2_1fSV5uFzAeoG|~yq@SjH4j--*U}yT-nr6IT+gBhU{$aJ!0f$}e ze-5y9A71)y$K$@4@;~JaSlBnDn9QI5R`kxgwhdoc7Rw2Hrg1nqFvK*si3u4mIJy4j z+@Fcx3JrqWFEE53;L&}v|5+qUT4bfdp%(dNq6Oz3@vvBXYN(jJO>?sj$kBa`~qE{%}vxqr1=eCg743=>(F?aiFc!tyasu|dsA&?r`Bg=h=c zj(=)FMGaHU)^F_gXcV5&>GI9B^6%H{JNJf9ZhfV8$*v>va9Oyy;HpV(E0cHXskL8= zda9Ihp~0SM!~Tvft)aysS-EkW|9m=qT$9^qO4Y&_q7$e2oVz*a$o4H>F>FiE3axs1 ztL%AbW8C@+SKo!tTdO9-^d;xX=jJ8a=cFcQr@p>_+27uFSJm&|Syt0_U&~fHdFEG< z&bu4Q{kpqTTkf!P864g*bN4RJo-o0g*P8onK6M2DcKo_vm2A?b8M9~RJ-Qa{EwZv! zDQG5#&A;@yl4d_9nFfi!2#`9pfnok#<`$b*Yae{Ak@lXJCOlKf<8N5zdj8eryCe!1 zf63#R$k3Uz&Gf{k^H0BdimjaB$$2-iT~>zsv*wr3jbQ>S(-n-PmEZ0E|L<V6}d+vz{y*neJ`s>wj_k3o%+S0{O3orOG%afx{> z4ek26$4y|Sm`a%0G1q0AZog_Wz4QHUdHiv{S6q`k|C=l4SxpqWrRCh*agK?BL4R5>DXKpo z`SRLjYEGOa8?Jf4Gda;NOiFCwwsVXomu}|2e$dR%$L*jYKihp)*m~DndtSV^`FKRw z``bpX>mG~erBrLSR;^)$Ww=9g@V(R za9yRH@!uyM?+{cz^)6N^oPXo1iHTozSZB9i`lX!5trR#XDA!sfLtvxbYAInRleP0B z1TMUb_KDZ43{{$xUA6b&&K!@!e(cwnzUtV{&d%)j*;+NLCR+AZn}_DST`NnH4R%@` z*yga=VZq(@?qlpM1@mGGEAzCRzVB?K-u^HiEC*7`mIkDx{ zA*kr|w4la$`2-fP`-g-yS=qf}uB*&zWb%^x s`bJ=;`?XUi?kH~J(Wvx!g^l;e{%n${PU0GsPoP06Pgg&ebxsLQ0G16e@c;k- literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000000000000000000000000000000000..185615e46f63c82ed3c573b3a40cfe4241415f96 GIT binary patch literal 5372 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000000000000000000000000000000000000..c95ea8adb863f3ff13de0196b7c5eead477c92d1 GIT binary patch literal 7005 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 QC_OTGy85}Sb4q9e03{nEdjJ3c literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ad6a290de1c1239bf2083e1368baa8d02b3a12 GIT binary patch literal 7880 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 YUwSX8Wv;TuO3*B_r>mdKI;Vst0AhsQKmY&$ literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3bdb4fad9eb45a63f51fb30f82f441cce4e406 GIT binary patch literal 7994 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 O3kFYDKbLh*2~7Zq8JSD~ literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000000000000000000000000000000000000..ea95961f24a3f43ae19a0b98994c3c3eb4e25c0f GIT binary patch literal 9835 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000000000000000000000000000000000..aec8236e31d4c3ecfd37aa26067b2d233681dec8 GIT binary patch literal 927 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe4mJh`hTcwDUIqpR#^NA%Cx&(BWL`2bFu0^f zc&7RKGH5X{FmNz1wr4W1fRr#WFi0_g0P_My24=7bBLl+%Cb+D~0%imoq;S*S4}TdL zm^nRN978MwOGB)^vmFKIxddKtlGbH;ZNlZWB|yMQK!o$ibA65_TphhPZfLj!b9hP$ z=(rpdVRSs`6cVCyL}h~71UJ^B?rSGaKQ}l3(L39n73}9McYpqowr}R$$4P>}`fF-y z1A~Kuo4=aRy?pSi^n>r;%@z9@tt~7RUMyR_+*E9V-t_J}dE14(uk$uAG!_>ZZ*+u$Vu8e)Qj? zM_K)Sd_G*cB69!Uy{Ns$Vpk$wtoZo#>(s*!4fy%_Kc742=j^aGYU}U4du`<;B_9T8 z965jf{3>$^mXe%HFHOFG{aR==bIVn(+GEK_b{?$7 z-M_!}XVQgf4*Ch(Y$ImW9dvhi`S@|OP-n}l#9#-Dva+&E@80Q&a0`nwj$8Nch0U+A^LLuRYu7G@K>xHIi*FuH3cT5=eE$6T&F7y#Uc6ZO@QnWc{+sW= ze?D`jhmWsq_ik%hk!uH5&F`20;YA9CESY+MEjBxQwd}OcM&2aGi7WT+jn%yFo08ACirKVdXWu0w-34pcu6@Y0 z%eO6PrAyH-(*qaG4tP9U(l)Ju@z184Yvjy-C~<0CdKnWR|M|fK1$QOEy2{Ff7rr!j z8|3HYe0cS0)!oD2zI{8!&CUJhYn5cUkTBacl~w0m*Gb35#l4!z#lmD2d!Z!AWa{xG z!&!6YEa~gx6Ed)!yh0%2TR!tEwm&alc3!)7ZOXYzmxOF>Y$n`E{ZwJIW#2wOW@hFs zn>IP^-Mja|#kg4!e=QrBSW>O)nP&R5J%9FW$L`&;pPGGm^G4_XojX_NKIM%+VsSKV z#mc(4M++{S{Xe>TwRWz>Iu#aY#@go#>YV-unQ=WA&ux&f`u}e3zO=vW(FtqxLn;b4 PfHH@ttDnm{r-UW|ePo_4 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000000000000000000000000000000000000..9f0e3ced8cc443ad41167f7c681b2771e2f280a5 GIT binary patch literal 10925 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 T9=^`Nz`)??>gTe~DWM4fJXd8Z literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000000000000000000000000000000000000..a82ce93b6e54b92c4a7f6b6b704e6a80cef29b29 GIT binary patch literal 12031 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 XX){xU+Ks9kpvC8&u6{1-oD!MU~ox| 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 Bnf(9& literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7f5e9b82b370a76fee8759b984ac541f74ca05 GIT binary patch literal 1732 zcmeAS@N?(olHy`uVBq!ia0y~yV9)?z4mJh`hMs>rav2yH7>k44ofy`glX=O&z~GV^ z;hE;^%b>-;z`()4*q+J20#d@jz#zo{0?Z2-8JNK$j0_A5nBcM+3z!jXkix2GX=@l5 z*z`PI977^F&qjEAT!|O?H*?w|wuNdb5)m1bGFxx>s0KmPNu{(<@T(sSke zs^3*Jd31iYpQcb>;4D9V+BBV)*$xg3Thq=;algz`P1q{<=lA#Zq7o7>CLJ%z)#Hr` z`6c7%!oyV55fdFf`|FE~%6(5JD!Z={R`)BI{X%h(9jDTZUXAuGJ9ez_wX(L({QmCl zjN^RqMMXxXB_$cQHx68jc6@Iu)bRi3XZP29yu5*aeteU;I5`)t4qt!i`T6-TPfyn` zd3=nQi=Y4bwl{BbUcG;Rz3jk;wiWIyXFGg-d;%gPXZD48dUDqO`4K2Q;mngKDs%1Y z<6h6Tt)6!5`0=GT6GcjvcAweAayD`1{Q1jYUtb@bk)dI2TlPkRmxm|AqqE2M(&590 zOaA;Q+!nb#Pd0XEQET15KPSsBm|Cwt*wf?Pl=an8V%xTDm#(dizIk$8?Cuu_o7tt9 z_!i`gTVGtaPVd&{bpGTc$BtdPySsdAr!|j;g`mk?+v;yU-`?Lpzd+hNZ^^P{YQAh$ z%hr8MJALEpyE{9b4Gj%@W=@;tws-H|TZcsFF+0xOvv~31l2=zW*$%#1vu@q8_xJZ_ zFPPfqsx2jXgX3Sny#0!G>-w%-J8bvq)2A1AcbBixwMtsW*<>qq{MfOK%gcOoB4T2E zB1Dvo&M$eB%T=?vIO5}`35ymjdhz!5_A>{2mfW+QYAEmf{r&yzSIQ1nzj}Xv|JwNd zbw9W4-(?&$_mb~{4_%DN%&J0d<_Ue>ClG?6ks~f_o!#AwpFK;vn-;(HQbUz-R@SQkCnu)loZH)aH{pswN zq)o$DY}`0eNl8h9C)%aFeEW}tSQpXxHx_(6Ia$5r!2!l?k^Adx@9wFbyy2zu1;(2B zcD0{+6&&77446DqOIuqqz&GX8*|Vy1PM1~w{bedDB9gJUTU`H>rEZmi+UnA)ER0$G zJW4gczGV6?*s^7c_@(RX<2h3tph?g&4hWY#R$47{ZiRo-r zT3h}7U8MhfyRCe3HW`lYrCqr-l}r`B0Y4tM_1xYf5MVCmy*4QO^fcXRQek1ipLeU@<)nBgS>=IV&+GO9sx$Zs(*Mj}Q+gYx99oBrD zU+-6;{j*T1<7bO!@6;|EwO{qBGgg0KjQ{XbOTmw&{LPJnx0|YFerySU&!LyR`onYv z?)$g4X3w0mtEC`v?)P(gBH3c82du60v?C)U_uN!dRSm4J-o4_(&XrYbPfu#pWqB{U zC)Q<2+^G#m1=T-&cc40C;f9mh`A8e3S*c=h@%Dot|*zN=R3K z|8pC$>*wZL?=+rmp0Bp2z18Bwhe{w%Lv-k$o bKl{%ZwTOAY=_1dApc=%})z4*}Q$iB}#*82B literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000000000000000000000000000000000000..4a67ebf1ff3082c1593c6123d95178cac73312a3 GIT binary patch literal 2060 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FU~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0pfiP8`}O!N#904^tPFBp6}md@emjq((sccJv)V7My`MPR zEVO_A{k{Eb^`|GDXJ!~W-?AAPJo$MCZe{ywo_@?aZdYN~3Z55npRl3Ti z>dT2=zqhS1JnMCgrdOBe}7uR|A&X!Blp$p zys|Dfx-@WI#6~B(x<4y+ZJ5bBaSP{;%GnPR?(eI;w6C_h`_++7;iv!p{$`tVS$R%c z$Db*h!E54Yo8_i_e|J|*WJ1IGlBGUOiVeo;=REH3tK~h=z{vdM&(F`#`VXvKxI^fc zqn?8L4E?>;-=_sH_lq>IU$WNFQ&pAG-tqn2-RjB5`%K$qt;?n~v-8h7S)wf7_hg}S z`;zSI>(a`vtOzXn{VjKMbd6$Gtcz7-P-$j*F@B9*kG|Hdb^*4fK$%V*sUH42QrdMB`pdT zFqP*AO4?R!xwE72@UN++YnIJOW3Bu9vg-LcSxNi4Jt6DkVhz8%yzIWA%TZrtqC<1)S)6Wc zQ}d%>XVKFxyTum^@@^eg5qjwUq3u_Htn&mGg)i6;)m4@=d*JxgEt&vtZ8QB zO*uQuw8@o)X_0%s+}U{t{{8)Z^}_e}_qQIIKV3im)s;tFjkaeurJjCrcX#=+$IU5= z?|*xFSv_)V*3>KOW~%8dFBm2+72c8<|3v-QgM-aaUtL|@#3E~1w1n|E z-!#syx($rc3on$tQJ!!{P`TaZ{*J=MCV6)v7?m%6{``4sAIs632mJ$@cyb?E&!6hL z;7)py%!9(;4;D#0{O~Sp>WN(IMLUb1zj}CYV)vcQv$wLYtXR0~#o@#IPcK}v^bX4l z$%L2(O4C~=c9%U_Bztvjw0Tkbql~}5zD|@Dt!0~Q(Qr9Ghb2KEur%1MbH~a>sto(< z|Hr+qV4G{98pgyT=Ev`6Y}dc}3}>SSj%qSOdWUVNA50LyW?qq_4bU5N-zD47bbrcZ#vV$P;*KDSG)DKrQXw5mA?8>=Phu+ zSu1W=iRT7QXO<5y@9nKVi4F@8ZNin|o;eI&#`a0D&5!R)zR4k4K)#&6TM`|%@C^+>P z=uIz3?OR>kcVda>WUrVPLH3f2HlG{q>;6P6^Pit*u5jaMl6iNmE{jWWW4oUhQ$WKH zw|jdk7l*%KWo7kZdAi88nOOZog-|QC_d-{k^@}5(}Ld zU7N9Jk{RPIj!p$}HeRU|mt7?13mOhrFp|+~Q58g~S zWGp*U^KE%pg>T<|0RvY9CI;^ZJErjqPpNdiJ#$K=@B(2Abq1AR*Vaa}UhZs&d^F8^ z&$RE%4%IWKJymmPx06t_iff42THx9#|3_n=-tA4gpD$c=)f8*goUUA!!c+gE?7-K^ zIml*oFn3txF9@oM NJzf1=);T3K0RTZ8(J24` literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000000000000000000000000000000000000..88985d899a650f753e81263822484c009fc74f37 GIT binary patch literal 2130 zcmeAS@N?(olHy`uVBq!ia0y~yU@!t<4mJh`208nVjSLJ7jKx9jP7LeL$-HD>U~ox| z@J#ddWzb?^VBlb2Y|mt10V!c%V31+}0p~9smxS;sz+S=K@udlC{pKDR*v^%vY>Xy{xkRT=@ zwu0Z^a+_|ykjl8UgmZ1|?rCL_5(}2Su+I43*Vo~<_EZ|1=HI*Xqes?y+Pc`? z%bplzu4MA!I`aMQ_4VQ6n?^ESr1v8UO$NH9K?ttkljRwvJy0NqglJZ*9x<-d*-~ z(?)~sm!gs_wH`d(ePNz$wT8L*bp~EJn;Gx!?bQ}aI?2nx7-jSR-rmK0vQ`|Ee|>#@ z`B<-XuHxeR;t_mzlli`?zc@Wze@pIdv)i+yw&i$6EMK+!yuyUPMnUaU7H-*cC3b&Z z>?PhUg(gLBR!@p@nHI#c{YJgX#uNpm*#|rIzQ4a;e)Q6Ojt{>c(?HT$$RE|=V6V33RIld%kPlM9-2b8~t(U&lO+Rjd2FIas8p z@ZH*&%)T}A@-YTQUd!TVJ)bIpP!#IA7ou9C%vrg{!HWaG`^HOI|_}W zwLZMPy?sl~&mt#=hNB-8zTMwZsGRDM$ngE$U2c^rTeGh(bL|#8xri&wd{>HLhri`{r<+1J-yx_je!Tnw`cgpDix#Yl2OfD9m=YL>& zI>BRa)mNjVR;908Zd^F@^gtu?m95#~i=Ab2qY@Zd*W^aBE^;c__EEaDCD&fz%lrHB zOiW^*E-ZBBWiX1%*|edrkC%A@-@nh{4J!=pe-{*(rW-BB!SSJfN8sA1ttyvYkF7o` zzdHAyb*Mt)+_|l(AzzNQOf8Zrm2sCXe|bsOJolE#?w$uP)N-}M))dHmmC1;FxMRES zOD!Qz0nsjL5sl-UU)-p@v&?t4%=}mup7$sE%CndmZfRVe>(R&{Fr|5tOsUN7nMqTu zZYAv1U|i8~qERAwUt-8~XX#h7c(i<{ykEoE8FVtF;;(%}!oz>zUPiG(Q7+=SMrt=% zxNq)>jFRYA*H)WwYO1z(O69jVGw;r6zIk7-qo|{RL!@8cep=O@4snJ>fvx<1H_3nK zZF}6@W58tG!1y^w{6vG!3+D9?n&&;XPPy2*KFzg8(jjM;Ln7O!>T7Evr%5lZ?nrWN zVZS6QGyUeFFJE2;XI|ddaKYaG!g5W{Sq0%XMNd2~GpEa6SkY(4zko+%e!Ox>?}XsVIJmf%i# z+bWk0d}*Q$L6$BrZ-g}b@O(VmJU`>*rKM#zlCCe(3|=PCuuvoZ>c#rwtV~MrC!U_3 zp84g)#k4e;Mc39wi+|qd&8o)KbTd@k(Q1F{X|YfI(Up~zCGYN7UcS10hH2H*76%9R zr8N#e3NA1AmuGHq7TMRI!MOI)ai&M#lg}J%W@nx+v8(*OpR23uDZdKMgzTO`PcE0^ zKa4pFtjpgmDSLZMaseYV+l7;p)u+aYG^~D+bX?t%l|!k9L&~mZM{oYsRZ}Be)9+1B z_Yn>WVq0WWlUlHKSJ6|itQ#8^YPQT4|0kgJMBc7OK_`T7;g?TOy)zH5txil_7_%+$ z>_YkJ0t{=9PG6XN@?Iu$VB0h?vlD|~p!<{)L7mGX9@*e14)k}2OaPqVs6wlX_; zdUAe!clWl)^;(YmyvC6ywA3$JZ>pC_NY$-%%sS|#y7hoP?}tOBHGf~J`OZpN?tYF(9-TNHRb1jX e%TzQt`44y1boslVv_oct>T^$5KbLh*2~7YSk=^_N literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000000000000000000000000000000000000..e5cbf6a41af399c14ec320ed7e32f82d8cb1c8c8 GIT binary patch literal 30526 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000000000000000000000000000000000000..dc079ea3ce00174757dc48b083b5852252fdc5f7 GIT binary patch literal 2298 zcmeAS@N?(olHy`uVBq!ia0y~yU@!+^4mJh`h84FjxiK&>Fct^7J29*~C-ahlfx#s; z!ZXd+mqCkxfq{d8u|1Q41*C+5fkBD^1eg~vGBATh7#SEAFu`Tb7ce8(AcdZ-XTlj6 zI8r=a978f#-$tc(hZZ+`b7i-^Sz~ZE$RTt^fFjrN_3`_i z*d?VbH1rqT+gF>d7q=%P;QFf2(10>x1q>UEG+Ei-s#M1kc0Jx=e3orqvjf+9k%vx>dn-SuWnEm<%CstW zw^`|%8xxC)i?^+@{P|&cc8=xbRbgvaCAaZNI*IGW1Q>m2pA+Kk_2ACbrb(OA z&UOV9eRy!NBr&J?1f5T3U-{o9AooPHEBH=zo~C z^835kSr&y#PoF%|IB{{Y`{l2%uTO2B-+F{a{XoKdZZVw&udc4v{=BdDcTi^LN~3lo zr}c9)&Gv{De|r-tWs))BRlrg&(OI_DW_v3?AM@%`x95`KDBN4|@lf8b4xJ52HyXC* z-(Q!-Jmvlx%|t2Q59Q%S@9*txv7LC>id*FT11O6>~v6$C1J+WZMnBE zU0&{=Yv0iQc!R9u4@VZ|Q^hr2GP9%hF1UYVWAa+IcPbrTfe%tTcbC6kcV1$~maMB? zW_fp3NSn8GIwXpfygk(r7ri}isafu=6_tk;eXx(~YZ&P(7+$c<}3si--A+$u(;%*|EdoP_`nMW%;`~pLh>N zGflA(OL)^8_h9Mq9!cj9i`@HUHvT#&engw|;M&;TVaE?;^U7ERqzimhj`=#*x_pXS z=!AD(wlzN{Ec2bc?7V2$iPuZZ-`tpJXR&9}fp7ck{;p!+ld*7^Yh4~Tp<-_2mJGqY zb$_EWV^X&oyB%Iy{r}%y-kkzfi~{Svy}d20q@;O)`PZ9UTeT$$5_&`%l^(e~FucA# z-oLW4auvs|7sB~mQQ?(=0;cac^{+&4%h~AX?eLJb{OhZ$Uw(d0xAvLmY1( zfE$a;-rdpEQ92WDZQEPVI_Cw$gdG77B0kJ(|8C3Jva9g%u`O9wwGMIXcUf@%RzK)p zd4P4EpSzi)II~GkRoby0$!DjgYPWrNKm2>qLyyD%(w8T$3SFJ{a>C{*GV0CzYgqXF zOqw_C;b{A=8oWI(_VDki4xOT9^ zLiz89brv=WA7x}(*40#YV}VPa+kwsvKG**@pZ?OLuFQW7>Ld9sv`+FYLED zTvzkBY4SlCm1y>c+SmI77O=|*Jr5OHCCu8lnALhk-QQnjW#8W3PXE6A)Ku-;yy5TE zpU4<6K1o`%ke#bNH_YDe&aM9&zjQIFt3^aaWSrl4evW1GukY{kEflr|vV@5^wVW&B z`^a9Tz+be{JZ@jj%@4QREDImCT;;D?bcBgrb#d2`whI5ph2NY?`fa#2oevUtZW#Pi zdh@gh0d>a*tv;q)+A@>ltPX}BTlD_k-s-@7VGsKzzb{ia1|4T-ig;^edi&I*lMH9 zODel36}aX7V$6MF)BHzGRkwiYQYqUBsmHs8zkgs!&zdy*&6SnGN3@c!ZkXTp(KJxu zK~iJO<%8n=MgD4Z9$b)tCLD;<|*@v<5g9G|S_dQaE8xFPYdh{yrfhdO^8PVT+1 z?^g1J85U==KDwK?32$z*KWtF5?U+Pw+P){eN6OfK2uoR96jaz-Dco>OR7_){td)h} zk$YE|m@XJxu2kYLJ$7KHlkwHQs`;mzopr0JaY? ARsaA1 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000000000000000000000000000000000000..de4fddce4ba58acb4f396557fc65181b9b381877 GIT binary patch literal 2470 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 E08H7PLjV8( literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9e939762ab0475869f28a49192ff72b839a963 GIT binary patch literal 2536 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000000000000000000000000000000000000..c67e407c57ddde65c52019d65d36c4566c883fd2 GIT binary patch literal 2614 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEk44ofy`glX=O&z~GV^ z;hE;^%b>-;z`()4*q+J20#d@jz#zo{0?Z2-8JNK$j0_A5nBcMw3z!jXkV3aWrwt4Y zoC`c%978G?-$ti*h+2!BTetW7x(#7(p0!>Xm zf>X6ZPX+DrnQ0WVEhlpIx`>TQCh6y7s=mIOI!$|~!w$8w3%?QOY>tx8`-)Xp-?_4@t&{q?HZ)0S!rYpC!ri2VQecemKXS65eG z*Yv?&o9r;Hs2+yedxf)lAk|5D(lAXGP!-_#ogWJvAaq__FAH4g)Vk%#obf3_#=tN!vr(07JG;{%zS ziO)_???2Wr@6Ru9cO~|$-7cdxrx=DApFf_SuD>Pgs@AHAjf=!Ym`?XB73ra3n*2(2r8eC*W0 zjXV+yE9OjJeWj6^o#Wi(wTzW-Z*9$deQoU-ub&GdDqUF{thLh5&s%zKu66XGkX0d_ zVe8|3#dF1X75kc2UYToMZo{7P?99wB zkB)M?&MoCQs-?nk=i`TmheNhzh4z|=w4ao|yHy(p&)AxKd)kJN4_ShfxgS~C7hS(}ZjR;TT-~O(-ww5M zUp0Aqd;9uH!rYq+G_@FJndh&|w5k0yWtrdHS#JXt$M3Hzd3s9p?z#2;i&pXpWPm ze4V_W`R_+>60WU@3|ZnK*sWgHDXt%PB_(0y1krQ+3_p_?85lTQ?g}2PVhC<=XSU1_ zySKl7J*)B5i&GW3o|!y4JMHy@gsC^=3m(~>QO{&BaK0}2(15w`BFmz%wNYDAPm8fi z{cCbEPgxtab&+#BU!nNc zx(3z4qP4G%bP8Wt=*;e2SXPjKf8W!*d9NoNU`)uIVOVEh`Ke`@&&)|@1C!$4&wKS@ z5%=G9vAdUj6MuH=@acmy_gpyd^GEMx@PU+5sXa?pPME&nh4GJqh7fn(1p0-5Qd)kW2D&@J2w(fh_7oIU!6+6IC@XpF~&b=MW_bt18 z#L?Y?{dmdYryH3jT***Ls_9_%QrPf7sF|H#Ok6KU;Pfo zxMa^!*|Dl=$qQkZ111w*7_d7poD!qMS=prWq;lSb;uxt0flm@KJo0uiQ!lc;-Pym& z=h1uzhJ>FBriZbRcdewTHY}IPk@|*L!zI}s|xWFE*XZf z3nABZKL1|uX$qsjl%|P4{;(!^?Mf zcek@1_#6@;6zcBBQCj};(ow6K2Mdjz(&j9>xwts`$)0HIq9+~8cGvzcyL!aN+u(P_ z)TKq&ZZa=ARJ1vjwMgK`!JR6%9)0vuWPHFk-@blX;p1ZyT_U)90wiC_*;FjZ+?2FS z$))kODigzH2JV=%Gwo`v6n*sDqwFoLo?Cma>7MSP$mo!~d2zVHqLP=FUj2(-QfxKZ zkXxRg;l*hqMYo+rPrWAH&RT22;IQD?jg85(RJUIj^;6Vg$lzwuF{x7)Y292V{8q{| zE8_b7Cv&bW-}WL|ZJ8)HgPyI=%&M=iB8@6yr*|%~dD6wfz@5*?mmeP=pT&RqnrNU%5*vfb-H9K&t}D9P zXI)v5Q_d23B7osT^@qly$!fkz8re~^<`K>9U~JCXyf|ia8gJ;jn43Xt(_{dW2;J}z2M-ESGxZ}?*?pIhh-GpIA<7mdKI;Vst0GtrOb^rhX literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000000000000000000000000000000000000..d09aebe2529fad76747e209af0feda3b13f6f319 GIT binary patch literal 2949 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 DBn3h} literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000000000000000000000000000000000000..3e649b670c64e7412d4090370e63a90a5892b991 GIT binary patch literal 3340 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 literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000000000000000000000000000000000..6dad29fe73c8977d94c44b69bc61aee1c65a2db5 GIT binary patch literal 3427 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 O*XQZ#=d#Wzp$Py3$KQMa literal 0 HcmV?d00001 diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png b/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a478afd6545419176d90a1f389de4e7084be65 GIT binary patch literal 3738 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 + exit 1 +fi + +BUILD_NUMBER=$LATEST_BUILD_NUMBER + +if [[ $# -gt 0 && "$1" == "increment" ]]; then + NEW_BUILD_NUMBER=$((LATEST_BUILD_NUMBER + 1)) + NEW_TAG="$TAG_PREFIX$NEW_BUILD_NUMBER" + BUILD_NUMBER=$NEW_BUILD_NUMBER + + git tag $NEW_TAG + git push --quiet origin $NEW_TAG + gh release create "$NEW_TAG" -t "Build $BUILD_NUMBER" --verify-tag --generate-notes >/dev/null +fi + +if [[ -z $(grep $BUILD_NUMBER Apple/Configuration/Version.xcconfig 2>/dev/null) ]]; then + echo "CURRENT_PROJECT_VERSION = $BUILD_NUMBER" > Apple/Configuration/Version.xcconfig + git update-index --assume-unchanged Apple/Configuration/Version.xcconfig +fi + +if [[ $# -gt 0 && "$1" == "status" ]]; then + if [[ $CURRENT_BUILD_NUMBER -eq $LATEST_BUILD_NUMBER ]]; then + echo "clean" + else + echo "dirty" + fi + exit 0 +fi + +echo $BUILD_NUMBER From a97063f9b731cf73ffddc4c81826935f6fd1b978 Mon Sep 17 00:00:00 2001 From: "Kartikey S. Chauhan" Date: Sat, 2 Sep 2023 23:18:25 +0530 Subject: [PATCH 11/25] Initial website setup - Created project structure with necessary directories and files - Set up Next.js with Tailwind CSS and Font Awesome - Added base HTML structure and layout components - Configured routing and created the homepage - Styled the homepage with basic styling - Added FontAwesome icons - Configured font imports and styles - Integrated HackClub branding elements This commit establishes the foundation for our website, including the project structure, styling, and initial content. --- site/.eslintrc.json | 3 + site/.gitignore | 5 ++ site/.prettierignore | 6 ++ site/assets/Bold.woff2 | Bin 0 -> 25000 bytes site/assets/Italic.woff2 | Bin 0 -> 21744 bytes site/assets/Regular.woff2 | Bin 0 -> 22408 bytes site/layout/layout.tsx | 47 ++++++++++++ site/next.config.js | 6 ++ site/package.json | 36 +++++++++ site/pages/_app.tsx | 14 ++++ site/pages/_document.tsx | 13 ++++ site/pages/index.tsx | 154 ++++++++++++++++++++++++++++++++++++++ site/postcss.config.js | 6 ++ site/prettier.config.js | 3 + site/public/hackclub.svg | 66 ++++++++++++++++ site/static/globals.css | 3 + site/tailwind.config.ts | 28 +++++++ site/tsconfig.json | 27 +++++++ 18 files changed, 417 insertions(+) create mode 100644 site/.eslintrc.json create mode 100644 site/.gitignore create mode 100644 site/.prettierignore create mode 100644 site/assets/Bold.woff2 create mode 100644 site/assets/Italic.woff2 create mode 100644 site/assets/Regular.woff2 create mode 100644 site/layout/layout.tsx create mode 100644 site/next.config.js create mode 100644 site/package.json create mode 100644 site/pages/_app.tsx create mode 100644 site/pages/_document.tsx create mode 100644 site/pages/index.tsx create mode 100644 site/postcss.config.js create mode 100644 site/prettier.config.js create mode 100644 site/public/hackclub.svg create mode 100644 site/static/globals.css create mode 100644 site/tailwind.config.ts create mode 100644 site/tsconfig.json diff --git a/site/.eslintrc.json b/site/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/site/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..71b863e --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/out/ + +/.next/ +next-env.d.ts diff --git a/site/.prettierignore b/site/.prettierignore new file mode 100644 index 0000000..fcac576 --- /dev/null +++ b/site/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage + +# Ignore all HTML files: +**/*.html \ No newline at end of file diff --git a/site/assets/Bold.woff2 b/site/assets/Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8c00084a985116cb2ef26d1b79acd67af692ba53 GIT binary patch literal 25000 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 z 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 VarXY^{9pFMN%PJW>}6qK001pXiIM;S literal 0 HcmV?d00001 diff --git a/site/assets/Italic.woff2 b/site/assets/Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..056909c32a24bdfed5c6149656be0e42cc6b47ec GIT binary patch literal 21744 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 literal 0 HcmV?d00001 diff --git a/site/assets/Regular.woff2 b/site/assets/Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d7c3d52c693bb4dfd0786625f21ea2f4726125d5 GIT binary patch literal 22408 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

Ra^9H{`RJM&`B<@1)md zLRJkoy5+7c%es5=-ASuwTSKm|e!cab<%RGWslUGcHo7(M`@Y0b_SdW5HwZ4v+j8oJ zRC@Tzjh0uQN1px0H2G^NyV}>6pH07JPQU+m@2pi(rCI)S*Unb^q^T^yzD%$DK#b?= zpWIzLErWTuPPTKeVXuqQ72Z&za^GXM*3K&F#Sfw-Umchjz-cER%I9*Rb~>Z1f%0?* zjn)>1#}RC%6Jj0ASffv-hc9bBbUfoRvl@??a@{5gR;x1??--q%C(V|+>E$BP$rp~d zez4S7t#D)6#k9^tzZR|S-?U+kZf;V$W%smlNn`tT`HZyHGZ)L8Pwlrozgdo5rh>6} zzV*4LR`m$KX?wLb{CEC+@Z9V7t<2?~o1e|SGqrrrWOlm`8*8qdDpI|7NuJ4mW=gXB z)PHh$i`btxi24{y^6k1}(O{O#qr()n^*o7 zbE(>N@N9J2eT#;J*=5X$!oeTE} z-k7)UtKHW6^9-h8vE5x-^&dDl-Gm1>*yn3=9nu8LQYBSmzk6 z@78D#WJqY3$LPn%z|g~U!I(kg!I!52oHqY1{nh=jPT{%DzQy);z8KiQYm)hGrhB0+ z`~FT1*K6IA7Ul*f@3$&*?2u}iw(@Pm$Nf@M|7v_V_u|b_wxeyQBF`(eH?!P({JO@M ktLMMn{5N$>s@vbz<)-Xu6W!CO^u65RTbIE + {children} + + ); +} diff --git a/site/next.config.js b/site/next.config.js new file mode 100644 index 0000000..a35bfad --- /dev/null +++ b/site/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "export", +}; + +module.exports = nextConfig; diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..5deb167 --- /dev/null +++ b/site/package.json @@ -0,0 +1,36 @@ +{ + "name": "site", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.4.2", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-brands-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", + "@headlessui/react": "^1.7.17", + "@headlessui/tailwindcss": "^0.2.0", + "@types/node": "20.5.8", + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", + "autoprefixer": "10.4.15", + "eslint": "8.48.0", + "eslint-config-next": "13.4.19", + "next": "13.4.19", + "postcss": "8.4.29", + "react": "18.2.0", + "react-dom": "18.2.0", + "tailwindcss": "3.3.3", + "typescript": "5.2.2" + }, + "devDependencies": { + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4" + } +} diff --git a/site/pages/_app.tsx b/site/pages/_app.tsx new file mode 100644 index 0000000..c0572f6 --- /dev/null +++ b/site/pages/_app.tsx @@ -0,0 +1,14 @@ +import Layout from "@/layout/layout"; +import type { AppProps } from "next/app"; +import { config } from "@fortawesome/fontawesome-svg-core"; +import "@fortawesome/fontawesome-svg-core/styles.css"; +config.autoAddCss = false; +import "static/globals.css"; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} diff --git a/site/pages/_document.tsx b/site/pages/_document.tsx new file mode 100644 index 0000000..ce4b5e1 --- /dev/null +++ b/site/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +

+ + + + ); +} diff --git a/site/pages/index.tsx b/site/pages/index.tsx new file mode 100644 index 0000000..73fbc33 --- /dev/null +++ b/site/pages/index.tsx @@ -0,0 +1,154 @@ +import { faGithub } from "@fortawesome/free-brands-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Head from "next/head"; +import { + faChevronDown, + faChevronUp, + faUpRightFromSquare, +} from "@fortawesome/free-solid-svg-icons"; +import { Menu, Transition } from "@headlessui/react"; +import { useState, useRef, useEffect } from "react"; +export default function Page() { + const [chevron, setChevron] = useState(false); + const menuButtonRef = useRef(null); + const toggleDropdown = () => { + setChevron(!chevron); + }; + const handleClickOutside = (event: MouseEvent) => { + if ( + menuButtonRef.current && + !menuButtonRef.current.contains(event.target as Node) + ) { + setChevron(false); + } + }; + useEffect(() => { + document.addEventListener("click", handleClickOutside); + + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, []); + return ( + <> + + Burrow + + + +
+
+

+ Burrow Through{" "} + Firewalls +

+
+

+ Burrow is an open source tool for burrowing through firewalls, + built by teenagers at{" "} + + + Hack Club. + + {" "} + + burrow + {" "} + is a Rust-based VPN for getting around restrictive Internet + censors. +

+
+
+
+ +
+ toggleDropdown()} + ref={menuButtonRef} + className="w-50 h-12 rounded-2xl bg-hackClubRed px-3 font-SpaceMono hover:scale-105 md:h-12 md:w-auto md:rounded-3xl md:text-xl 2xl:h-16 2xl:text-2xl " + > + Install for Linux + {chevron ? ( + + ) : ( + + )} + +
+ + +
+ + {({ active }) => ( + + Install for Windows + + )} + + + + Install for MacOS + + +
+
+
+
+ + + +
+ +
+ {/* Footer */} + {/* */} +
+
+ + ); +} diff --git a/site/postcss.config.js b/site/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/site/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/site/prettier.config.js b/site/prettier.config.js new file mode 100644 index 0000000..d573118 --- /dev/null +++ b/site/prettier.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["prettier-plugin-tailwindcss"], +}; diff --git a/site/public/hackclub.svg b/site/public/hackclub.svg new file mode 100644 index 0000000..38c2a68 --- /dev/null +++ b/site/public/hackclub.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/static/globals.css b/site/static/globals.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/site/static/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/site/tailwind.config.ts b/site/tailwind.config.ts new file mode 100644 index 0000000..3df6f5a --- /dev/null +++ b/site/tailwind.config.ts @@ -0,0 +1,28 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + backgroundBlack: "#17171D", + hackClubRed: "#EC3750", + hackClubBlueShade: "#32323D", + hackClubBlue: "#338EDA", + burrowStroke: "#595959", + burrowHover: "#3D3D3D", + }, + fontFamily: { + SpaceMono: ["var(--font-space-mono)"], + Poppins: ["var(--font-poppins)"], + PhantomSans: ["var(--font-phantom-sans)"], + }, + }, + }, + plugins: [require("@headlessui/tailwindcss")({ prefix: "ui" })], +}; +export default config; diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000..c714696 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From ec8cc533abf7502cb09ea6f8033124786afe5fb7 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 30 Mar 2024 17:17:52 -0700 Subject: [PATCH 12/25] Add apple-app-site-association file --- site/bun.lockb | Bin 0 -> 140507 bytes site/next.config.js | 9 +++++++- site/package.json | 2 +- .../.well-known/apple-app-site-association | 21 ++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 site/bun.lockb create mode 100644 site/public/.well-known/apple-app-site-association diff --git a/site/bun.lockb b/site/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..ea2d13747933513bfd419def1a5e0e9b8fb67bdc GIT binary patch literal 140507 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 z z#vFiT~uJ96PCRU#-x+eKDqT%tqdP_g}|`DE66r@Gf-K z;<}6EUfBK?kkKrzmqO~oS?(S+)>77T;%3_I*J}4Zq@>C3;`SLiKithbt+HltZd1Rr zKs)&Bc0;RajN2xBo1kpvx#z(HwRIcw&ub!?8woNHidndBPj1U#H{<)PIJF^8_rPW) zW9G^q$K`IBYkc5N^_+Tno6b?^mY{vFYur|?YuYgJ$65p3*Prdo8yBW2+_0CA3q~>* zwC@ILECY+&<4flaq}4-yu-MC8+-=2vVe7;ctET@(6K4rVopW&N=-9RFm`r(q^`^(W zewaMvmAu_(#vo-Z`>yqf^@$hdpCJ7q295>>28L*up`d(`Y+(J#z&YlalEQWWl1T+p z%xC?}v{){?wVh7fU?nEAKl66Ci&}kXC;R`jCASZ1h5wa$V)x3s?)7B(Z29*^ju3M> z7(nL6pqM*TX+qOcrv0Y6O|02`i~rm@w=JXRq55~J%X2Qx{CV-sHwjJcU+s&N9<>!S z7_6>%N>%nafhSE3pKlwIQA4z_6 z@l<)U`F@+|ixLd=ba843#DwmW=+D|oU$OgGnL6@9=i_nEV5 zS-dHuhSSIU8~6;G_ySfWXYtS0s8C=1sBtM*XO&5+;iRey)=OP>osQZ6SoRm#Towk9 zdlQh&&GcoTP&?g@Cu{BIK$fuYoAe#p-XB`Jq|)VQ@2&H09p|%DG#c5$a$PN23fDJt zzX`NgnA~a|mc)}BQ6chnrV(V^2x2a1e-_MWmc2LAmBJc?iBYpQS)RbR-m=U3&^xS=4!aLjLtN5+1ud%;5omXNJWS*CS15|H< z_Itt2?bkW?nn$8tuJ!j0=8xAS>t6Lew9Zt$V}4k9?oK)Rqoz!=%eoJ5Q)+MSrVFGoh|)i#|(g^vGMpFMx_PJGJj)LqxC7j8BE^6;v$GHA~NtUiYA zLj#%3GP|!qbJu6hm~-dOA5LRUm!A0Z$=#g&!8R+Wnlm5q(A;f4YXcL{^$yDyOZhdY z<|p2r@n7%I2|tcFJ@wN)-!`m*q%%ml4%){CG82SZbWcBfb8wRXKlxqr7u72*x+U>R zfk&pKkKK(s-*1x05s^gpC$3vPYT_=OJL-OCYryi>h<%P6KfdXgT5ErvFtZA>2L)m- zXkQ%2Ob}*q-BkI{vbEr{Q-Nva;_H*=2%Y>>Z5$cP|Gwy_4G=sd-A8&)Gu$DD~=ov=^z84m}Q#lSDC3FP8`=<&;K*gE#}s_!|Ns} z$;fW~8{cwmgDOZvW?HzL~pAC&x)xv`v0j@ZPfaL^nA7Lfo4H zH4{X!EX{kz9a%9$tY#P6qqOZDo0Rm!WkY6~e|YQ3bk}p+w)88jFC36t2xY ze|E)$wyC>|OQMTbZQgHlb@!t`jlX)9|K~7H7TBt>DnloG?eTXX`n3&^%*}$D38Gkp z>uY>H=QB3?v#J+2zL=+-X8Jo^vs|d~v+8Z_pF7``Bx-6dxsp(}<(+EIhTLiH=N%1B z)xHy7_1D2O`DM29kuD^2LHid$W`Z!w|C)aX^Ed0Ds4Z~b?1Tg^-TEl=1~4!Zuj@FgZ@zTH)|lW}TuR=(Qmy|-eW zrPSgn6+2fyzOe(z++3)cAd1ECxaEQJh0Cu^KOeU7<+hNMeNlD%dAD}|ELy^GFC{z4 zvFX#+Z7pZbdb2n5UHbQ9%g$ArmrhB1bNuwl?DU+U(JaT1%muAm2bl@NEF0#R+;Bf} zuGTgC)r?u@0juP7?M@kVq>7}X} z*r(d(n5OpnA(;!>{|Pb^gjpEe4CU`WmI!|SckQAO9 z%;)fWt^M(3_fM=*Dsij`yS?Kwi@E&MY1W6AB&{?yKwcMA05TAYS;P(qKbKV&&s1r- z$u;Sv;$}6zwNh)nrH-`zOq!>pb#aea-qX{~+T~Jzd8R-6d$mUS0N1k}|IQxKXe)o; zCb8Vt5XrrueX3An8CW9x&0p`U1bXXVpbZK$V{@Hb6dE5Tg z&z|*te)sy0u>6iodNth)Z^bji*yeDj3oS=7w*+b?h+;{u7qwNqv&u1>Yht?d?RTD) zt9HHqxv;ovN&4iM3sFg0wKiue_e2g~yX+fw59U+Us`v+Dd#m(5$G|N$&jiB%W6 zDrVQ76Hs9KzEiaD(NXK8y!k&fJayx*y{i%Gb`_Ql-gb7|r27TgdmqT&DRD~ZLkfp- zkbzLl5*I#Y;rYA+sjqr-#4}xPGUc2p2&?=&Uw}FL{qa!k=Rz{wq9^R%bI%EQ;5IYl zzW16}*H@KAtBC&Im-&wC{pA(N^9&VGGeH!~K6hrjEs={uBz1TGblKRyjQ`q<7OQ8g z#G=I6H2>dz`YLsj+v<4Fz2{G5a;~nJ6}&3w=*knfXEeOM>aKM|R_FRNB==T=41{8q zpIg4|sAGAuvE`lo^SiI_c>fldGWF1suBub{d=c~F!q@M5W34s))2&RqS1JO!xAxp$ zxc;YuU}L+$qvPQ|+cO1_+tr|b*id5`Sa?+5y=Z^9@}$mNGq2>rkIQRzt(p<>R4H=S z)*il1a|A{DVpC*|L~WJ$&nNBqbolquZ^j*`WO*N))y>gMTzJC`GI%sdoGuo z`r$QV!X2G;F%bdMX^&4gzq``k!W7UOY~q|??s_0hMCYU>-|sc~a~SU2VdU7T*_>a}eyJqyO$*0m&uU55*|YhwEGngA zt6UpQi;wvruN$m`847amg;mKbOG5W;GvYe>bu&wStkIUe>!v!n@Vq#j@#fdG_Zy0O zI5HRoYy^LuzjDt`D`@e@mMy6cYhCSc1idIquwJZ)!w;PW1GUg=Z(gi zFs)z6>!Uz@43L>1%yMW>W978{@#D%o%zwwB3wS*{-ffo+oPr83@HJpWlbAe0E#hD*Vfny2y2X zx38^H>X36eb4Tg|hv_XJnVN>?SozD-CW!__7kIVRf4+V=P?>T5s@va=u<;(=_v#dK ze-m^L0Mu9p7SVeO3}XLI9&DJgO7G)i`7PQjy|jK#mC34aYyS7Wq`-DnapURBn-_Dw zZa3O5xVT}3;qoR8gOWcEjh3N{{aN)vdzWG940Nsl++4NPGwEye_E>gz6sk?Xv*g_3 zUDFw(7yf0FZc-^oYcahrk;k$ptn$;lbM6~D*g3bYy3E^i;)nApkKiDdZMwewGDz_T zI-dY;t^wQk58^^OwQ@2S3g_=ydgJ2%iM8LY9LlH8bu_(EEpUAzm*C~3Bk!X&>*Xmi z7+l#lE%b;GTdARQ-J-Q#CTwCjPDn@x~M%3QpkqTKz?rnn^3QE5x z_bjWfwm7Z-_J6hc)9o7$7WLI{*FXE~o^+=BrLfcs6RMRPH@f^?&@SBd=h|N(m+;aJ z;VIi+{hR+KIw{2BLOt?0Ogpl;=KJR^<|IPE?dyJO2@2D*?k#b#L3ZXm+{N6EpYmb}eAHPwDXX*gT0XCG8aR8);6_u)Ff9 zYqi#0HvW~jR@H#Lc8ZNyW#fZ`J9Kk)|3`9f7qYn>-@INvQ_dI3yZ4~tZ(6y~K@Nk7 zJ}bC1mc?HDZN2KE#~r5pGuDfm4nBBb`}uoS$meZM{dZQUhtvemPj+WjxQaZk(T!}b z?&9RFqElG=_nnbT7yevOxQgdgNdM#~`%;P>uiD(rIpi~6Z|hDKlCApj@=CCK*~2?`miZKf=NA zMS^o)OYk37uBj4lI(DMl_~gbN{%`*!PhBy6gWcn;vy@)?lqk8eoZ{HNQNLM^AvCJ# zwzXVxByxMQ582!|Svh}8H_hDiJc{ai-Ba=iykK#<@wk1pZ{}8 ziZHWXy=J{ZM|*%p*TJt@uT}QT^v=FsTHWa9$#TG}`{c2otk0aoqs+RmT}Ey{Oh7hQ zbtb3bOa_TSfBQ78*s14pPWrHM8=mp!&^xDU(E9&YFZWl6f-moH-0RyQ&j09Z)2*dP zIitB|cUQ_DX}-(7b~1APHWAres}Fw#?wr@%zChsS4g0AgHItOqT3Gz7;fY9_P|I@T z!t~`wZogMp{5`n-Eh9_6()*|KqBlHeu8a$1uUf!SvgROiew&1Bu7k#;tp8z|wf+q* zCx2@B&X?_X6b=iC$S{4Geb z`z@KtqolyQVr|%2b4HVzhe+<7hHUP{XqlCj78Y7nAJt0AJXka%cKtuG_`WX}Ys$&h zwf%pu$wiuM(C!U8X%jW&Zk$_!W%^Z-KU@FuZw&KFt@U6tL>^b3j%;os)AgX44hLp1 zoxEcGp_%hEn^WN{t;tdBmxEheuQaHh{~erQG@aw2htgr8TN|3smUdne`hWdI@WjQ^ zdrveO^&qd8n}KYu!Epz-r!LMGy}pl}KUnar$|!J;b#&#w!}5Qn-ZfcI%lmf@@qPRl z*U{{fTl*{Za%!#rZ|3XY8+|Ok$i8qo{Q-GiZzi(2i+0bqh<}9vbpQl=lqzQSntHtX6+m=nX10S;#ynEQ_b*gbGhYt z8lE0se5mfdR?^BxlV5i)W2yN4&DL+{25z~=t#6eypYa4-2c2O8%NL-1)-a=4#9bO@ z&Q}V$vj1N`^9+>(mn;)s8l0L?Vx+w$=4Bigugy#i{@8h6|NgHQ^bOID%KO_C%81-~{P*6Qz^1AHGyQe-UoQCxp7n2z0H`CT5+ZO-%v-hRusu1SJJq4jr3t}$?*KX&q6F)VjzV^oo zxlG;e^_P*ucRtKekbC`Z#hA>!&TLaIwO%Ju=458rVz;w;;R0p}oU-{5&u(_|pKsh% z#mdU8U_O8LTYqKHVt@ECKYQogH$or@kAe(zqf5J1}hphjOvAI`h+q(t}5kVK7 zfSKtnJK~nNSr)8+eLDD;+4p_^7f&vlX77AiTXpS>Ej<_a&nk5}vUj(sjaxgidqMl~ zVMeob3Y=G2${*tF{PyB`>r+p1UY=jB@Tu{@(XAOTE0dj?^~LP7t{01*{%Eb@`b71` ztAjW4{sqjs)pR!N`oz4_bH`pIrH3UjLqYCk*<|h)8ysEye)?Y3`&!$R*D3v6qj|7u zjf9xkB(I)rW!IZFGym53y-sw?q=(GMjiM3@7YpWPG_LY8Iml&l;!zHgxl57Fb*b^u zo1Z#o;W1gs2VWTzU6iF0*hQDs?cJ2pRd*b6kdO?u44`+h&bo$sxlUSQzKF-3XZOTPHWk@1txi0@b(u}(t6 zZ_@JdB5H&WOG+d zXxJ-RyXkhH-1n|cb;su(3f%XvyKnNx`1!3_ zeQbwf`Lq;bQfhy^{(wAxyc*eDVNFi|FQ!^1*NSdsHZ&;IDsS1cG%wZTVZ*7f@4P1O zN>+Wr{{D`W(A1~aE2kPSEI8`Jc$r6!*Q#-Ux~RF(Paoud;TmLf&u*w)qvYTpb-Pd1 zIX@#yBk*j(o`h}wyn!;2ciIa03#-iTM8{5Hnpd*u%1*-{4KG?RKNXw1?KZ1}PDfnI z6d@s`@Lh{+uB#xE-HG2_??2|x;z~TtDyx+#a`R39zcR~^i<vub9XnB3;u6fOPw`CK-AVb7hjGG(qB?VB&4xy+&4py^BIJlVWZC$q*$ zZIb&Y8(O6LEMaR_c!Xr`24r*JZJKgg_q%h)LvcY z$?JT_b;oA^OIDKQPWsNw|NES!%lGZb?WoPj=1vo@KM>zN`96DE%b{(d*%{UG?Ne`b z87XIl-kH2xD(LpCxeA|j&ZNtUOcyZ9k{9?r>qrg1chjtuDTfTEewO}#ybo{-vbp)v z_rhCi_D_?3RL<&a``T7RlI4}tB=b|3^)y05Jr8s@hlu#_R-Hdt;{E-+($iJ7v#&ir z;iw$>b7z!8+Sc!K)Eg zy+3@YpYnmF`*O;p8=1d;Uy!-a*rwCI`yzS(sf@vE30<=uCW&wTwwZySTR zNut6vpflfK=59weH&x^9%$JUDn&KmDTlen!m;PX(Pt&y3E0s&vSJrG7(73WQOMI$a z9mm8Uf)2iK6nZWfDt*vBbThy{gzf&+_b=*@`^P(w&Hc^&V2QOhdzco(ahCvfb-$?i z5W(MJD}OP64!&bA7Fy_$u~=1T^NCd37aR?u9IvC+O8IPk8L77@F1WfnvTr(adwnOe zxi3H8`6;K=&ei|o{w<}y^RF?pe+r(uHHCfAf*mK;t?)1nU3hvK)A2*AEXC)om|-N@ zb5Q@8K+(&$;R~nTeKmch3-UbKE@X4}K9gu=+s7ZmsBQVa)$wEg@mt5DJ_Oa|R2WDz z&9i*tYoK#u#m&u=v?^}!2`m5XXjr|raLvJmPX0-;k(S~X0?7Nxb|ag+bpM&8)Hho= zjW!Avewxv%9PK7@g+qk*{RH1*s~h#0!(Cr7zWo3CLE362iD@hM>+{~_-?4aO!M@e~ zswZ#06*EFEANC-dE6LjuDHRZY{>2X-o21q8sgv)Uiq1@OdsO0|@@!Y?KY{K0E*@w7 zxa>yzqgP9({yfDj<#k1Kf}wEHi|sRIHcBQ!=KUe_ID3)JwLg%|bo70y{!|51j_VQb zoQ(SK+k{_l6X~AhDRQ-A#oq1D4y5(g*{zoM+4VE$nf50W=a}tsR!5mG++Ffcc(+a_ zQoh)SY_7VdpMcNdj#r;5J&)${{972&6sya%=}YOXk53kySoi#?65F4BuC2efTY4xo z>THRB5W044{{?|K-@8pU`7hQaiXoZ1AKBdNzZo9h(LN(!ROuz~swb+YXxZA?Z{}=R z@KF3?beXF}=7fpL0XD(j;TNU|gz#UNcS$U~ym(gC(HoJ*!ExW_A3)B>2awHO5N@q> z=dZIw+3^luS?&BgQk|>ku^g?O&;4wxnSpNhx()=_T-|1xMdwCr86=K9^Mr)>J& zpPqX9m0i)~m@RVn9z-_xeTjg<0&b>FHvjjy>0GkEvuEiVH|;6Qg7PQq{k8VomggJ% ziUqo-3;$ebE8Lr-qq(%Z$krxt8Gp>8boHXejV{Rf?GUoLuk;f2i?yUbZIkYO7FoK$ z{?(#0q5B=`e@{^MyMTKS zmmEemclFfsr*5?hG__AiPcrDbaouyhuJm&!IkW!{SFiv0 z3t}t6OA6E#S2fQOdi$!-@2}*3qyhTS?a{x}lYYA3^6D+ebfs4A0iE3lE6*=`TcO+z(kUc^$>G_SdyvhKg)6R_&Y6=YC+@i6o8(GmeXXSYmPQS=afP!oZV% zx6Dou-6A3w&XRkVH5tjh$B@mPUUXfsfIW(#Do9G3D*E0&O=cJYMy7TIMCY zQ0bZ z7so>7mYuEnaj;yxuSM!^gV9pA)@KP@c)yE8ua&h;EuAO9?!Qa@cNLfX{MA2^_qUuv zHuvaB#=x4%N=yE}yS43j0#E)6)7ND`Rbtj8`5Hc2<15Z~-^sH#tz)9>zBTqnwtrd6 z@20QY8^##W(fr`fS!JzR$m6M}k(0&Wc=h1HyW0>>q zceuuVY85zs!y#av+@x2pc0CuC<~FhpJywd89?l_~oBX7*W!C4-b0=`*F3s5aW9K!| z`O@<9^_`Zy@NM;e@>gY2xMG!m-qEvSM=zavVW%FnyY$Zj2NkQ@uFrmIeY2|qk<2}h zY;IReU|})ei-$#Sb2f)pvRr9mEN^Xl{!?{Z&E2C4s}}rcGRU^q)9<_ zd#vqDwy)VHdGz(-2migcAg?36fNbu{_aTv4DHVA|XXomg_BhYH_kQtwb{=u_Z)qh* zQ&%nuU}G?MwYHScO6}&Ci&5!u`c5qCUpq)$mL zovg(+dGmBlwGHx<{PI724m?-zMb@n4$+?TykGh#1cUb-4>b*Jjg|{m`*9KpW&W(L_ zcJAaV`RYidaJYnQZuz=svmmK*>6iH*%rdvePFu7peBSoHC+X>-YbUQZ-Oy6Af_ zUc(hVS67S1Uw!h5@8t*oWR+q=xy(0*G|du`*RO-lyM`If(zi@rZ{D-Tw@;j8|8vT9 z+x`u9H|`1aKA5LGzxn*t*ICM2yEy+`=XfN)G5Xt=zNjzqW;Z8gH-2+#5ni|T(cY_) zvyt2jIyW0`?iGP8tbvK%ze-l!v8t-7seaGL@2ais#j0-|)N|)y!2(1i>{PD)` z>@F?oakCBdVu=d<|M3S`)!Rv*^gVr$%)JUT6f~~EbKr}Xa>dIRE2}Lg`3If1|1!D8 z)MV516P}fey|}M#YBk%oCT1&pgXfEDg`abeHmwpgHVqcB*)+FrtwPw6GAqbBJV?9b z8nU^sqx`2R{B55bx@h|*b%($z^*G~VW{pG3+t*M1ve)K*ZPK!J6Fxi)PwiM`zw-aq zjUOMK^Z528u~>Z<+qNg&mW;^zd9Ndzo6i=z-+@bSR;p-kSE}eQzd!5DcPaPndwR&j zYF4cFf}XvJuP3eF6m);@swr2uesqyI$Hw*DqOI`NwUv{X3Dt+1A%(*YWOKPc^s_CR zHt&1Pu|*8-GJWmcUjrTaII0qO*NP=^dGQ46oIQ2E@cV;jFABVv59!!Wi`B}kiRFDY2mJ(ytw|={o%Ef zrzU?lS^TK!T3bbN-q&A>g-0Uxbj&pT+&AZ$$TlSR-at%4wG7rL09V z_cpS*4Q5grMQQWwq@raOzij*z8x`xgMmJ>Rm8Zx4Ra^T@p1J?>#5>LH zsFQoXLH&xYgXVvx^h$}fELjOi=H5Xz_l&&Ly2nzpY?ghF*rq!7icwvK&B8U#)|^#+ z8~5Bjt}>5_=?E|Tiix{!mHGAkw*1h$t0!mKA;q(cUs%U{;_z@q-bV#HCmv)x%U10} zS{JgvMY3M-DaqvM|8l=?X`FF))_>FIJHuR@b62guKgB!Y%|j83@HJT4qH{vi2!|{4LYxKIIjgoa>Z;tbNWd=Ucf~>+V?{ zw)?rJLBg3G9_l-Oseirf@M24ONbq{j)1C;3Vr zdb4@{aii5G|G)fIeRzG_)lbhR{^t?3*r#8!*mzsso#NG(4HC>E=ByF|$TNY{!x97r8%nFf>hGQeV77AwIQbV)i#)#jTubm$oa4 z&A)nl)3vAk@}8yd@;8JtJopsIrRZ{|uekgc$M+KC^!pImT&w;6CVl#7Hhsbu>!6dj z)Qtr;{=OD&bw>BV%&%{iz4uAwW|lFl{bUvSSDgFRe#g%BbA(crdsfdaDO`HB{m;2v zotA2N`w*Bw2!N7&LnJ@pR&)X92T`>wz<)2;K%OB&IwKi5u?q}tvdyIuXdilbhJ_pV4ysNV+JdgcECt*4#-B|UjKd)WizeDMrsC@4Ls zh>Dz7ULeeRAvrkD?)IJjUvpT@4$ttuw)02o+%k{Z`M}o@u8&@?wnTHEH!#6$pzvf^y}#zUGs18++Ix?=N*nSs?43X zUfq}+l^*!faW3+>C+J=VxVc9v9`fzIV&KKb4C z7{j{`kN@{(kNkQcshmG=$$93Ea+AKwgwwWq-xl@gdC9M27iD#M75>xI z?w3qrDw4Uckw-g_(MR_R`p4{3w|BfPdC5`ad*PAOMIr)W9v$`H4?kf4>mqGa9E&_2^%mLO zvPJn>n{FMfX3e>9Pc`o|vpvU_`7!1B5<7pd6SpXlKIJQCkjXxr?CliD5>wYzX&wvZt}* z%JE+D>Vo8JSu@%r4o}zCZZxgG6EXK}z12;}usss}e~Q`Tg}!* zU)1eyt*Z@Kv3Kt*e_qzuRrOmHk2M^sx$HmtP(M;QfbIo>8O@SD)A0RmgW$>E-X=<& zEH>RDu`~7k)100~W~}C`Rj*k7kG{1-JXx`pU*GBYBZ2*&?tDyWd$Hf_JS+cFmAx(L z3z6qHKfw$Ixz|B?=`=RatxGk&Wz1kbbg0!-O6JB%@!}hsLz7DE4jfWczUF4A`9!$r z(t&CNKKW^0F0~3bI{4==+Q8l<{7EGfvR?$!?)!{v?$;J6Z)t{39htql-whr0X)f-`yIGK2B6ldNq2thr$D1}CGM%}=XRX$L&H5FSxleblo_2zvt}Ojly7Q?} z{WXefv@@O;Me;r6*ra}}VwX1ZdT`LaDsXcz-ziB=bm_Ab<3D^jUsL2z@7u4dPR~E= zaV~R)z{JeNmwb#IFDImITFc?PP~^^Gc2>9dn+g-FKeAQbYn{AO;~?_6KHp%5g2Gql z_lErBe`Z?@?p72^p8fT+Wc?1|7t6Y@Cz{j>^Y7z(ACdp<;}f2Pb(?h9I=%=_RjM-) z`E~I0?P#6KxsR*1_JQuKfR(?X`(I#2v#`ot-D5CS=KZSM0xnS}4ZRnf6porWci-XT zrrL|xeqT{t;rU|@yO>S~) zp251d*1tz?PVS$+q=>#d$;(6qrwZITkYLNdFUHniV0WiXXU%PaG|N5xH`Yv;rGcBf$ljWZ&4csy(x8rv#sI-L98bA@zrFR7o8I)Rg8SSG z^>rl&t_C{?w_Kb0bH8(adw8Sr*T!PuIo}g&-+x$Bx28JEWGJl7M=+z`6Kx$zE5Xe6i7AH&a?UQAC`_nCi4Dm-wDL**vh<2kjn ztuMLMAg|N?4KoxJZ<&4@=NLx#*Ob0}fB! znRs~eMD2Es%o@XoYaSa&LEV^KY-0u5>Y;H}EMd&e2m+DKpHU954W?CGO zym(lB*p>GH-MuOH>vTK#4`=n(MY?K-EX{L>|IN07o7 zbiWVGXqJF)=VcE4+OVHr`DfpXKRWV_ySi2HaO9lss^{9{d~eP(-)B)yCSsA!PpfWy zPiBvtAh&asBzNVXN5xgC3_lj7LH5-_@-gV1Ah@|b3OWD1QfK_;o42W#mmzxgf{eX{&rtx)v#c}$~1@13drM=|6zuL z!Z&r6!IuJ$C+}M9SUPpx{>)GIYDx|BXIL+j!@6UpPv57bs{-3z&3LrtM{e0^xr}Ye zUTTLwK3{MyO*3$ghEl}0LZom2U&;nGn`K?gl?zKdtdc5tg`OQ)Ui#&A!sVZn?=3#N z-9`FN>aX&=?~xB%=c5?%tKy(2D%RkYAgeb zT3(y&fw|nZat7OO-j+AMnLY6UkIsFQgDk7pZTx;C)az{j$EltUlD~y|GQHwWOez)k z^fFI6nY(<>Lf7b-?u^Lw5EIhD!YrFuZtH#eRdDme?Z))n1)ut|@0RlR=pS31c0x)` zih<>n=5+fXjuN?RTlQr|ePwkvW#_6Yld0Jo(CD#opVu8e$Q?e#e$AV<`|RE7-SXGo-CR{ak+*5hxoutQ{vVar@!!vm^gX1mzi;ykq& zq|^)V*gKEC9(f-XXiXE`+%NviE56%sUsrbuPcw^~-8ALKYaJFjDjNmp5!c3nSp{*c5+iThh*?U2_$fbP+PyLWre z)P1+zCvxf@D)yZrV*7!^#bmpGpuuI~+&sGl0vA{c96!!;SQ6^(?7M5@?_BfWZoBV< z)%sbn?|7nlu%ciR=&mPNeZh(BUQ>m@M~*)iMtlwCe*b;1_D)Zx@XcFiTZoFhvtRrA zTfvst>?_Z{RGohB=D+Knwnc@+h2ZR<_l}%f58OH)@LN^}x!uQwZ0_%Lbsx95NKl&0%m|r}_xh&4zvq2^L1zAv z5{c#C-y!8=P@02>L%}mwM=oPOv90yZhB`4%8Nhz zBg1evvqLW$C*o?t8bMLNXV$h6QeJ-$CWmH_K0{9&vvBCiZ6I z0p~l`V$&mbYiiy~6?OIa_G@eB42~-IrVD$k-YsAEdAYIUnz-_+{lV57E}h}Nq;>iO zlDVL|4sPznJv;YZ<=lLt)p1{NqT_M9=YH<5rYAIWZBsSgvq@6XETqr#=xmEkEVH{? zLt=hdo0JN&PCLnLGHHh5CF9-yGa+{%K=Lu@3>vt($wA>-+doYdaw`k?P_oi{$(|l1 z)m2fyCzZ!t-aFIk!>0ol$t%QLKV|(bzF3jb{emgUEVM!|NclyC$n*Dci4#F-3zi;0 zV`*@64>R5nXxZX?R{mL*&heuEr5|^0pY>R9z3cY5ZoZ3iKd)_>H>XW*hVe~F$vvB2 zUTduMD^T+5ND)}-vinqk%_M8&{u`+7fSY@N=A=pQBxBa<*J!s37#M7y7u%e1Hp}ql zwf21R_d;I#&4i9cJmj2S_jJwTQ!M3YF17~G_PASvc3k?- zd_y+m?L;?+`ee}EXRvSptzm(?_oL(TFK=CWqtBSgicH!4V_#38vH9*FQSK+=(makN zN0jnRo5dWa`RKG?&3o6>W8x3|if%7^BYf_|?-xhjMhn!>L`o0B$nJezkkQ=HQFWte z@o6+5WBdJ9>SRvdBMzhRj(L%~nE>b-L3#B!dQ zFHxP&Cu3rMrquN1*>kxqb1ps6^R!k^nR|qj*>l^kx-0*X=NUkAzHoE5+~wi6nKrNI z|D(c7uinH?30)C5kGxhVaYpf9&WjCMJcsCU+?Uib`+mZI+ z>KUmoO$(9t9f0aOxVifA%o(%AJw45S_paQ3vU~H4ll~c5Vva74Y_}eGxgghTO?rm+ z`{bNJzdz6T1LL^NH@o>ZMpoRkXkT8IYxVysa`_;E9B&)12#Lu3n-+ehknxJc+kfl_ z1>J1#}B4)X}evD*tPue_IbQY{p>sUY+KAOevR{2hR5X0 z(5-J~uI<;ItMR|5c>TkU$y^UQn~=-}?MZ=~d&zv?icjqVrxf>o3Fh!J=h}SiH z4z)9DHoB<^c~}a)eArp8>N$JstOPz6&IQchnGW8xV-b9_jCopX=rM8RbS8})4rf{g zC0e!$hgd8MJ-K{7+xu`=?)&RrK0}zjd+io~Wdr zIrLt??3|QDGkaQbblH??oZlk1X@1%^odLOj3|i9!4~I>AOy68G@pWzAk+zc~ux*z3 zS;ZH(?z{G|etR?Jb7K9X?3J9}<}+m0{aA3VtN)m4RP0`jXxEEc7k`;m=sXK*LT;ym z*l=^d^e2i13GaDY`E+IRf(0Hw)@Qx%)W4+EQ`t9b|NM2c4i{u=SAPBT>w9{Xw8q6> zEo)gh3}4jv~4ZqVL6xVek}C&e+$D4z6RU7+3e?eTr`0{?T;p3Yx+ z{qPYFQ7h5E-(TMgn4@@5$lPda@2cy!XT3T0^pDy4EyCTaU$I>j=tf>w0UC>do4a@Q z0YlMq4Who{D?bT%cHc4Zx_)Eg5_1h@)_t2<>^L_r+PyP*_v$(|Plqc9KUB4@&Ag%e zpk->%(bFfawm-ZcgFFrcsypE3-qvYwKh-$l?u}jIomr)t5qcqaxBXmn;M0K%HSPvi zr`Ryd?vi1ceQ(u@ue;Ar3bA`UG3Cdp-|SpdSFOxyIJ(yuIbWzCr-$ZXF@p_B=MQ>y zhwnJ-Y80~f!!MGo|1-{S z?7qHS`G=}RURB)H9VLDjrK0}K-fVE}(G5e6*%Oz>i%Pvdo6#H3RAe#LshB6P(y*LQ zz9|y9{{}kq32ts?+G54_H!GAT^0CVIxSlotl)Bz*ozat))3c?^w@%Nyx?P~I;l-%} zAvv?+UClpEO;~(|M}7G<9rg}&&5xJ(`apdkSh)jQQwleiBjR&ngVIBb_)hLe=QajZ zeYqS{ozAs%e)XPGCJB~}M?Sxtb~^VLx69^tjV4BHH>&E7xwwW3olHE+|L@xF?$TJK z@YO;N--w8BU1z@Cn6@zJf8+b}!c*R!eQor8&Ly|t19MV0-`pFh!t$-NM73hSUco7j z+|yJ1L^n*Fms@`8KcnaAo&N&@KzEeF+zT3GriFV!YZl<d#!OV$D@-au;>;O=!ay!iFH@4VWdt~RL|o*!>9Y+MoPdQ*}2qM})O z+@;k#J#U*QDJQl^ifKNPm^k6t<$!DayC&?v6SXbd`KA9uF3?@Muy_NV4FWfJS;nLE zJ-O^h1(WiA*adbjHaL4q`c24InI&R-cqh2s-P(R|;jOQmAGyt#RM-9C=Z}X69tUwI zO+4k_=}^d)maKgq$-SVnAmHY1e6r2Yc9)N+A9u1xan`x&oNX!Z?$rwkZ`l?jsk~gK z*w1;xl1_*C!%hY_Pg)&wQnY7!sB!7i2jP7!Ug?dTS;*!ZA&0MSnegMJ&UaBB8V;*( zC5S67RxLg3)c)_tg#}(#@3=gFvd{RxEG=ct@1vCs0$N>18&5m;J}5Hp@NeQt^CLHTqCZYeHOn!Xe_nriwDG}|q#1wBW|h1-Ub=Sv ztchjIY!9d2-4Ptp^m#+e%?q1sn3liPLSByunmdGtgU$ibt#WSWCvRNHWz4+EcCkqA z0XwhZo0iiBpIR0+wBNK?vZaUR-D~lD&pig2DGAG#PkwNIinq+ad0bOwq)cJni4?w| zG81m@35ELGoYMO%{5Q#(rRvLyaz5(68tPc&WBxDa8~Hy;^xO3oMCe{hAqj$;mE zGXMVV)zd}P%nlh#`WF1PzXrO)7#0p@$l*}6=(L4n<-bsy-rR$S-%IWMr<$sEdz#_J zTgSTQcJ-OZw*AfKT$4VJr*xg;;}?Sevlr*?Q*%3Cn_{5hurr$J0_aX-n7N>Ptl{om z?RD&+`rhhEOa9h8?T}Bru_5N$&FS%ZZkNw~U%yknc4y_|x}&yf-?r}Z2*1O?Xg-_! zuj!|Wv%db}=X-t3{Lx3`^)R430ymd)_l`MT%LT93`4qcs5KmbYc`i=EMEU63s>WLa zEA%%@<>aPEJ?IwMb$8D7bH|VC2yp$rx5&!if`!4~l^eGzO-0`4V~HFNS2s)&RIRf6 zzvy=0`{h#Q{PHV3>{S(h*X)eAxL!bH`_J2(*<^lQ+LfI4w$tfxdH?)*Yb$N*m#<_v z^yc~7x0wb7Nbv@m>x8>kt#7WnAJa^Ycjsz2J#}9OK3!J+@k{NOrxQyb9GtPQYHbKt z)`SBZbrriMEE_f?-!JV>jB4*FWpQRY-Zi-;?M(-gxz@<;z0dB}d97&m+?RPU>^#FMaih;w&yHNGS)DyiLqYkD=E>wo zDyr?6KvUYktUT|7-Vykk4_kMK-tjifQZe*{AFJJhcn*JKu67wq>lA3e~vq z`uM?PEi$`qR@Um~@9eFSuMyf_ar8uV_GJ04^RA`R=NulKX(%yPLf(H2T3Y}Q-zcwZ z%P&rRu=Ku0nz6;s$vaY$uDZ6?bDO{4`M2;~<-cpc_+D)iUlrp#LAs+*r**;0xie(jOn+h8>N<1!!K2miNDUGwGNzST~(#+pL#0el|XUygn5)R}XiuOwPsyPi_P& z^t?J9QoWF~sk}m{eFH<^XT$5StY5PARYuMb`26=s<V5!t=WCnDdxEDV%-R5WMK@|m)>hTr2Vj(wi0sjAmHzqPok@rQrC zO(RpiP4bUL;X6$~t$mnqwJqtE^LIy`3x^(7)`RMHn7N?4p5X4iEMC3x%@;=go}99^ zsg@qsxOeVz+_QeOgsYQ-%m;QA$u_;SRp(}EFJblGXTs(1YF5mLE9}gR)Qw{<`#hIb zm|XQF`_82OIygo4(7Rb%j=AT`UH)>b($g`dcUDuLtxjBi&q3zxPtFTx zn>s$~PW!xZNo=>-^u_%LSYq#gz7i$o7>bmBL1$vX-K(M9&wk8FX<;mL;|I-Z{qsA1 zHGOF|6FC1i^Gc6ooSV~P+uMg`6y`badr`Rh#D|byrnjH66`BP^eUooCDykPnUf%~= z`w2HU_m;}v>Qf56&x2;F&W-E8C6jA#>rh_vV#YI#J2|fj3GaG6v0$xo;8FjERkMxu zyz6dAN9Bjvs>qq z^Xp?O5;sq-JfJx>TGGXIQ-%EGi?I`Atflk%-f-~tY?mw8exEGVJq)Ha2=7c^!7H@8HGMf9)zLVHW&@Z)TJte33456pVEC5zpY^-%ln z9gDL|%Qo&ScH1i!`RXv=^nfMv9S*-#5lId{Ug26$@@;i~22#9%_K?HPo$S8)7ORTx zf4<8`ogEx$6D?0pwLW2z{reQdS=;R$8|yEt8Fr>rFZ?1q<>+oHAt^0{_Yp(7!Lq6x<8`)fuussedE@*$Sbb6S5J8t)E z4WFumtJ{Jb`QP42xa0Tz%Z_-%BeAcIeC)se7p!{R!Sr!=tG(@=?1fu(o-kj!mc0(i zy`VLZ@Nke6YY^J8&cEm7mwTGm^ci`?)P&Vv{1!1kQE@W3eUbKL=j@QQz=`{Il{v7w zoO=9uS3>L;`^7BEPlGn?ypMq7V7$4Fztr3C-S@=XpS20Ugb?& zgWmG6{JABv!eUNez}slE?Qb7MrtW%tS~DYlV)HJKueVJTWcNNjRKp=>#4LAEK8k1WRTDM&)HSZ3?A%trh&{&Y6F<+p>48{_B&LfV^%Hv~~mTUKNEJ?j_6lTI4St zE-G4He}96tk-56|v)~nek9S_#wRrd1qYevRX?8Z3aNSyST{hbv)%(Ubn~_y-Z?>ek9bk@_4U<}U)hf# z-^&F$%LH!jBRyxUJs-@wCNndHVH$-I0o5*--#7-&X6`ntV zt=h*^`uO(4MGJzrF(mHUq@TL_jm24{a0o$;Hw$+kcJ*&9?uWCN9?r>(G(Xk%a?9f> zmsa(heKh-$+po9JIQANC%m$n)r+G9K<; z`>79-8Vvq@zxI0CC)P~?-28L0*u#14U&*ggW02_H-u)v%rTHiNcxm|vH zV!iiY;Yt6Rp4)Qof9eRTD`DjsXl?**?v4uUr(*wOo&;}=uC~+p_ttH;eVMttR-*arr-=u(q}qfzCL~CJ#>`;mh9ieVlNqze z>hFG=%{FW~ry*{+raoqp`JSjf3daLBGJHGx?~btTMPP#YU=u6OYHTHj|1 z({_H{F?nlI@wFzC9}C2zmpx(J`!!}oVYzf(jKvg%Xr=b1z-#{;^Tn)N1K;q6c&&bM zabieR{l4#@z6#9TDCBUEa^1ZD(XaBf6HotNoxEa+`kmbA+vDSI3GOaq6JGi1()^=h z3rs6F^D$x0WqZyI!h) z{c=Jh^^N{@^BLd&F}?fJW%EwUm{BMC@6<$7;U&`E2fzLGPpQ6l^_GkAGvQV(9W{33 z`_y8P%{BbuloY}r*YtGh%Y9Wm$#sr;_E!Zqv%H?7yk=7Mnm^Zh7VZp|&oJrvb!+K% zj(k5Z3BDsY(h76fWhAFxTiBt4T<(CzWZ>b;acAdV9p1aYMZ7Lw)UCT8*rj~x%?p7e z)9vCudu*!l;|{E|{`O$=a?7O2x6KSP%A8a(Wlu_8o^?9#!vBW$x#pm;Z&>)oA-nh1 zIL$qWw;Wy-mh}7g($t=BR^1KCt9rJ1D9rg? zJtb{fUdJ!pBWDgCSdYA}7<6Vj+`acdPRqX5$L%J^zv)Wd=e8R+nokK&bNgL7`PCgQ z;dM#H61O`Ram`4W`&3@VrZx87>J$a_9UJmrx;F&u&Th3o3cCLSW)?CHItLfThtV)L zh!4UbJ_v)@9H4a=AkHWru^|9T>K0rK4BQOhBee%zIDp)@kB5PQi-zt4*}=rez`#dC zI|g0A4uFF|;k->~@TRo^2$E4Np&W`JnJi%FH9H%pul}!K4o4Uq5{Y2F^iRPBJktFgO@O z+In7wqv?7urJYf?jE2By2#kina18-a-KgS=+QtNl4d$_t;Tji1)nB0Wf}Vzks$T}e z-JtO6WrVc<@YxSiHyY0PLWp*9!#V6heydAn0NttBKxznr^rDYnVG|?O+)?#|BLqNw z(}JSZl9J5SqU1dC)5zfP&8RCzLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$( zGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!n zMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ON zU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLx6Aytbkq|&;mW^pUbAWC|NHvuec;JCr2+Q zKRG)sGbdFqq$oAjPQl1Pp*XWDH9t*9!9*c3Co?@SKaC4v$G`s&01^S+R{}blg&)LW zU|@jpL1%m{gNlLf9s#LgW?*1g0Tlz?8v;@TIy-+QR19=C2S^<&0|UbR(9FcZ(89#P(8|QX(8k2Tu#SO&VLbx_!v+QhhK&pi44W7j z7&bF7Fl=F9VA#sQz_5*hfnhrX1H%pm28Nvs3=F#%7#MalFfi<4U|`tGz`(GNfq`K^ z0|UbW1_p+M3=9m17#J81GcYh5VPIf5%D}*IjDdmSI0FO22?hp+lMD=|Yx)fkBRefkB>u zfkAY?&As?3frB?3oxC9GDmw9GMsxWSAHjWSJNk z6qpzo6qy(pl$aP8l$jV9RG1hT)R-6;)R`CH%x2$VJ%7#LU|;|pFJ8*Pz)-=!z);D+z>vnkz>vwnz>omSXABGs5ey6rp$rTR zz6=Zuehdr@o(v2OUJMKj+6)W~It&a9dJGH<`V0&V1`G@ghM@RoVqgH>qYk?Bynum$ zA&-H90d#jT6X^aPMh1p|3=9l^85kIzGB7YaV_;x-&cML%kb!~09(2|%0|UcL1_p*# z3=9mf85kJeGB7Z_V_;w?U}9h>W@2DSVParNVq#zb-2n-@@8cmO1H&Um28PFs3=H8+ z3=BRD3=Gap3=A$z3=FPJ3=D2e3=9TL3=H~A3=B6I85ne#7#N-~GBAMdwtmgXzyP}I z2GBiA9~c=JJ~A>efbIhY-QAhUz`#(=z`zg(GM9mY!G(c=!Igo5!H9u@ z!330685tM~85kHqckI4rU|@K}z`)?dz`#(>#K2I&#K2I)#K2GriZ@0E2GAXBp!?E5 zcbkFk^#a|&1-jP?RMvv-2MT9oU;vegpfV6t=7GvHP*DcDdj(XKr7$uuq%txvq%kru zq%$%wfbKN|-3bc1|2rF$rWqL+>KPdrzA`c}EM;V10NpK^#K6Gd#=yW}%D}+zn1O+z zmWhF(jER9^J0ki1X11LN|+{?Ky?YIPJz`S$aM;+tOJ#K zpgIFokMKe37Erwcs#8Gq38-EHiG##spyeT`T$F~gLFFN+Tm+SmpmI?OS}uahN08e< zZU&W!AoqjHNKib0@+?RkRQrSSEr@Ll&ATAKfcT*FZNb36V9vn6V9CJ1V8y_|U=1zX zKw$(b1Dv60-I0NT0TfmsaZp-=SR!T4XU?6bvURl2Zb4^o(I*Tp!#(o0|UbX1_p-t3=9l& zp=C3uZUohnp!yP2Z}u@TFmyoEC#bFk)wwOu@*5P6u==wely?~z80r|nXR$HVGB7Zd zF)%QI@*u2?uYs0lRZz8{GQS*J_QTkXNcDLqQk~w*z`)QCtV8mN zKL=Xhfa)C(dmgk60Fndw5mYaM+LNF*2CTgSYHO^4wjV+5jI|7qHWDa2K`v}%n z0=11m;Rwn@ps)q8L173AD^NIr+Df3q3qkUrwiBq$1Zq2h+D#xgf#M8=LG_amBcz=m zzz8YRL2d-K3qX2d^%kgI0BRqA+6F%u7#O}o+gl(rK#yA0Gm1Mxv}AT=N}Ky5To+YHo31Brt$NIl3LP|~f$Rm< z!yvc6fY#NZIvdpf0o5g-x(-xt!`eZhwhpNM2y+vt-2<{8)Sd+S59BV8ognvu+Djm@ z>(Dk6sO@wM>V_N8_9{pp2!qsvXpkNd8)WW%Xj>K8E|C8~;xIMHe30ERIZ)Vw;s>M- z7aOD=6fPh&AiF^{NDSm=Q2PYL2k8azLE!}A!!Sr4$S#mPNDRaW(I7s^UQoP(!r?Om z0|UsfAoD?LK=A}(gW?vZ9wZKmD^UD^#6WE1G9OePg8C+)HY-RTW(SN$7DHx((mLp( zD^UD`(lW9fBO|1ZjVuOo2Pl2;GeY_hF!Mp_4wh~~=?y)dv4GlI(7p!93{ZH1+T7@7 zgY<&h;h^*j3Ijey1_n_0@G>$m@GwH!h@f-{3VTqR0kzjbX^k6nHZCIr1E@_8YGZ=T z1=#~K1H=Z=AUi?j42TbNH^`45b?9QCwu>wy0|O|0KxKgpBLf5I9z01#1_lX61_p6x z*nr9eP#YJyTmbbgKxF`^-7Ce&zyQLa_BY5ZJy4~<$iM(HQ^n?L3b;I z%mmfvAU??dAR1XcEDS*9C8)g%ato*(WWWgNXMx-U@(-wvGX%9e85kHqZDP>9mZ1CG zL3gi%?qCGz2dM*zfyz@54HF05^9?cwbniLnPIXW)fbJ>>-75~dqZ)LtHpos;ISpz@ zfbQ=GwOe6kfclZ3HZ!Q62eqF;_alSaGNASfsGR~52eqX^_wRz*)u8)*LGmCypf(|> zZ4J8P71YiKwf{h46QI5gsE!A<4?yD*FgB<>2erXLd{8?a)CPxPP&rgO!Of&P>ll&j`};QeTo#*hKBPd*M44K?6QFg z`|&U^G)Rf63N|VSpJ8G&w9qrsGcpAC$^{r08aj3_yChM5 zQ3onx4k-sfdcNsyXb54beOe{dI z2IXTR28M?BHaTUQi=QSlF&Y|!^jI?FrB;9%PYjJGzudGBikpj&DabD_NiHsCh)Fzm z>WvRaBiJ-OBTEK`qSVA>kfsUGZ`_ldaAXS9Z3YYspgaw_XQo=h`d4OqNE$*0l;c2Q z;PvBV4(t2HAxw(ItcyE6 z{%u*RQUi7YSPy1w$60`KmN9sY26RWFLH_xf6UA#m{xLKKJI)>IHtCi1_t!n&$^dnn zAp=7o6L_q!!T;yst$SFKW+7xiqZ=U8B3CQF;%zP40}VGr28JRg@VIHik#*%xLX0le zFd0LJqSWI2oU&8~4a249rC*eO2fNw`6m`X=$wi69sSMkW7I4q@6)gh$)EJaepF>TX zExP@cRO_B>aBegP#r8jFSYP9_%o7sz_Jq34fPn!VZJ=IKc<(!@NIzF5MsqzwJy5@o zof$j^+E8^{Uf@i!ZY9_SV5wYCHv=@97<8reV??PlIOfbju~3j%lvt8l%uxUCb)}7$ z7^pk|r2|6-1_@^HC~Cv2z~t(HZjCyy+YI$A85r&}LsHe!Zxt!Qe8+gf_JHH(6*G8L zwjrqV+B&%iFD|I74H+1|L-nwJPkHj{ai;-7Pf127+{Uap8g*vrJ|zycoM zZNP0$4J*V2tk0*K<;_^fgD@>KFD)lCJ)=Z)sm`I-k1kw+%9t=PWaee07G;(c3(k5a zE^+@#Iuj$PZw(r1Y-eR)0OcQfrblss@7@)FoSg}lG0`(LV7SHs z9szEs{J8bTZTmftbYKih2X>s0ypZ6T%&pWJdJ7zG;PL>MX(kL+To9iw&MGUd{1+_? zPKkz~Ano9S__TRL>Uk|o|D9kL7=S`&5*K*Px#7n$wd*&&r!rUF)U>QSDz$F(KrxqtOFa&$t{KQgc3i2taMmJz! z$l-ywZISUczxhjlnuGO#OH16j9=${oV2lIRjfN%+d5L-XnJEkm@&58EN{6n2>R?c1 zVg?xl#a_OZ^Fmy$eRXls#q3Qn2p3>2dkyqV85k-Pb3p?z3?{#L_J&X07zYVakWVq) zW@l@mXP{@vP*SX$m!Fc#&=O&mCV1jroCu?xt)(6)!CCM_T;O+TbI7$lNwdMGffF32 zX^^tlkl_qJq@-Bdxj4|*BdY}Jm81KsU!DRhBapn^+5e&0IHp_l$PMUfYlz5 z(-;as#RRDB>;3H8wq{Lsu-iaMhassbF*&uEf#HD24Ypl}pUZ<&A~@z!1R;4lG~vzv zUlS6)LG>6iFx(b|xXt$SfsLmGOf#4m3jV75iJ6$0YQ2U7#K8!A*ON7eR!*d{jo9F9&mcYUDG}jhLj=QDN{D?xpneBSP$6M z-=H%4b!OkP4wdi%hno?ouE$cNgQ__r28P_!qV!ZF28J)ogyt|!N}B-I0}dS=wKJ$p z#NG-pWMJ4X0;wC%^7vkf+WKCTi4j!QgGLI|5{pYfqq=2}ye=E~^~QqjF$0x}MX9C5 zpeW*3aSgGbssl^CCJYRiJ~h$VAH_0%n?yYX=yF*5mzTf9eLZO_6JwpRo*}5##7qZupuBCwfTbl=2MxDhVvyK& z;oq>lr1wG>6Js67s|E}Vm@!uets600HDG(d={hkbC9|kNNq)MdtWr=n*lpm@Nh?k* zOU)}O-Z7y`y!BnjJr+zFVYGHp#ZAKcv5oJ!MO`+nHZCyxAUTxJCL4) zp$P*+jT9trGcEgYDlKD&HmL0n&e{xZP`CZ*SU%~-xk6D;E-?XR1%|48&}<|FgRa@* z*X}O$2SGgvBSTP|qF)L;OWhE?fg>U{vn59a)Hef%HSQ2a@3A1lx;U{ITm=R_cj^+H zm{9>X4HP{L3#GubxDE3ntgbN~PL^YV_olW>LCWh!!>c7C+LI#?GN+{=b^ZGK`WHf+ z?`I%no=Ab$1~hoIF?i`LkC=>*`6mTw1zc|u+sJuYhXo-cD$T$k!NAZk;lqkEZe?eb z5i+{c5Yv2auB>zqVnTA8yEMcF>Aj_wdnRTyAoL_aO}qK~`!TM!k6RHkm*pTi*#1X> z_Ad6f9pE%-0!j!E0atSv{YC-C zZSvr8mMeVDZbIMWjV};+uB1386EsiaHvjIbMdG`EfO9=KSura? zLNsjVlLmg7c`FckL0SnC3qp*ZcfM>-Uko+Pn1R7b2@;}dH|;lt>RLQyVl+~MCH*ta5DyVCCZe*BaaOqPOj4xQDVCUu1`%tCG2!1NZMh0`bjvHS)>f? zHX~5pTCD_0Z{Igeac7P$djswnfwSX5B}iTJ=!4o_E%W@_;F{JLRN4Q9x-G2aU1j8d z(ZfuPSCqi(2O3h*MhOHM#grjsA$m_-fYHtt)UYyS$W#WePiQFK6S{gG6aPnWY=gR4 z4DHI0FnHK;yJVx^DGsoV87PG;P=YTe87qR5A!`Uh0{8!<3!P=@4sG1f>~KiTG! zOz?4vUCNMfv-K;elx;m^0}eMsLp?)-t56qM8G46EHQrwXjsXbMgIhwMl_9A&pe|?6osFA8_JGRlb>8tl$n>>6{_i`d*LvsE&GO4!6T=@Bf_l54u^M9rhScJM zqSRCdhGpWeSz>!SjF=eXEcL))omyO2np#xJaLxGZ3*OBdso>B7kBg-imzHGa6f;z4 zJ&>+Ba0E0;0BRr@F)$daKw1HkCnj>frRe{GwC_hF3wyPTojemIby4oZcc;An~Km&ygfQaq}xk z3IUb=m~9Ghy=}n2kfH)9#YDPqJqkT@bSc=U2B0=yR7W-oB!wHHB8|6fz%i( z;MF$`VNxC!LQIQdoO9dl`+QJO+!WF#Kcx!E!8;u0 zpYvwe^A+q$guk2elr)4(a@u_`2mRP6j}&T8ceN;{xbWyZjO zJ9IunU9kCN{UoRKHL6g13>g^yLS-zb=}u8RECdQ`(1@l10|Rb7xMeU+tAm=BSd?Cn zSX7+K#Q*MU*r9u%91L=`Ap?W379=Ge6teyr?=uNB%4VQv1WLZWT9EV>7I@k(QDDn) zXkIX2V3+}wajy|xm=p&t-#~3x0|tfy-JJa7#FEtO$~Nq+mF;cd)C(?M3UqTab2CdA zzQzXh{4GAE1x<-Y3=F;65PS5O+Z}k;np6u7YXb%bIUPvM9oLL+D1Vs757lGBz>t=c zSW=Rj!fp^H6$cvrGG<^H%ws}7bRglDt0|YXYD$tns5J=ArVN^TkZ{YkozhWw z`3@-NK>ZTX=y7s>5hx4a{NA6kYrzvxZDyfo32Iy})`O&j8B-jNT-gyJ0ggFv8IPk> zHe_H(L^Dl*(L^8820I14bv^Nr;!*b*lAxRgsxb^0u)7VMPqDfhEQ7lw&jq(r85ovq zIdXOD^?nafTNzxqFcc(~Wawt6R$eOJ|L6)Sf|Saxhm(`fs(%KJ-Gb5^Wb8>dEhjO3 z@|}!byb-rgf%Sk*>obPflT@f;>%h%#0d;{P1H)ZoNNit>Sarek%lxNIjCF>3;9dca zdd`r6A;<(0Pc8OWq`fYjaEF>^z`&4W0;#uecBuWci|zo08>kF1WMC*Q%GAv&W?)d~ zZ8#RA-37|qCVJp{yP!0$GC4mbRnso1)0~eL)O!cDcMKR9?93P#WEdD4>^cG${fy6n zjJkt+p;+4#;0_Sh8Ux(1$K9&IY8p88;%GTQ$GoxE&Y-o_*y~^gb4c6e-nP~J$w5mO zfMXl%AKdjRR{ww#sg60Mz2m?3V)@(alA!qo(3pcE1H)N!NNlfGj9B!m^}h z*5GUfNxh;Hb9Ka+&VokfK(01pUr}D>0|6$^X1w7 z+@HZ<8E|Z;V74HkX{W&o5;~$`XCLb*9|4U)8iML=BkW}e*guP{AmJ8ryGlb_#v+Z0 zak3SpzQ!F38=(IAz9Pw|<)(ZC)NN)A4EwAgz3R;qH!|@~I0u^L0hJVH3=9{b_PlxS z#Hs9AwF0Wgh=JiQR7P98U~)`wC8$LV%8h0W46m#p`BZmZ6R)E6A<%fDp{bsQ9=2JY zFHlp8Q*+Bwix?P0J)eg0PEvUcu7klL%4iK~yDU2Rs8a26A1LjB>R<~{TN!mORDe;+ z8q(LCV$A?$D*NR;AJoDJg{Tn&!(3}fKgi>Vt(aetA7ma7T=uTBhLmF6C+fJb zpS|}R>{BC9lHX|!3F|lOZ5N7c4Ta1+fFu}>LG`fDnRrII`Pm|{9&oGkB2jxM~9lQCXS4^%GY;KMD0Is6C1^ z3~<|n+toOHiZ|w-*g{J3qJOJ@U3=66YjqkkFif(8((v`_qM>J_ginPwXKj z(}#xB0a|K5K{Kl$8AAq!|MrlylRvZN#S*y#u$eAXoGHYJfkD&(lDGLBroED`4vv7< zfQAeV>JE_h!P}H4FTZtE`+(yVoUY9rAmv+}--Fp8UKuQ;INuKNgDG!YPZuC_03PW3Yae)WN5*te8x|x3Do1h7 zP2rAHa?&X7e3}alo!nV_6elL`I|Qx)!8xnO0g`sOZ+@8?v5gxtdkJonw>v<};5*?K zYx;C|-i3y>5qLDbC^bE`V!PYoIVW^XA#)Pow##&AxJk60n7J=vC8*>Bg@F+R!&(PO zo)fv!9^qc0nh1ebAaUSRaYZc9qZlzYN3GQ$IyV`2sEs3ici-QYDjwm)nmlK zfVDg@1a$&&=Y?|)kdn{Lr%GFI{(C)e9Sn9g?o#Zw1Ehql(A9ojqa_6ynFFVTH_#BR zUhMCcxb6&S&IsgF0|tiQ4v?AE6`v~FBW5aY2fG>^q5_VPTvFgrv-&{2H6K{U05r;` z;RtEjNvJ(nyf|66Xsrh*M2#5m)SC01A?ZN0uR`(P z&9nEw;ReogY%Y+z@G5Z~t8K}$t56wG561-(*4&S*UwKcS2&w15y$=N!NN!{fE$@0Z zO$Ic3337o6&YWe+z+eEiXTeM^Gx?o8@1gd9=ESkrb4CmdxWgK^4DN8lqX&D48saPc zai<*|Wr!v2)N4rP)Qh{k#^E+Y16(a3BL)WCKBcZbI9dUQ3=HJB8f$wLT*6{+kAfx~ zai^WZY#P>Z1D7G0#kx4^M%+HdlS^>6{js_L+7jh)>q+9y|{Y?m0x{DOlG|g3}v#U4U+8aldG6;?7-=cA}XcXnG2_ zf5=IR}&-VANOK}MWG%Sf@TcBHNgu;w{%J%_tLcRT>nFX>R@WLQ5>88p@jYN;CFD`Ck= zZ@9+-NF1vHmm#?O_4Mka-wS~B^c?k=GIG+t%Yb7W-1f)a3&kC7SYr}bt7&U;I8LzyA5|NEDMB$&Iyhi;&u_);fQEq~Sf)6&gn?mC!i)XwO!q*uNuUvQLk5PKL6Fg}1*g&l-tJ-n&Bj7y z7DHwJ#rB84zT^g*Ys1^0!2DvauYVe~7^H4|$#(9wxNr@4m5!c)o&^I#co<}?=1ode=v|uwF5p#1;2BTcBiooWP;sDz zWX22(Nnwyt!*1=1QcFzzjhGm1L3!Jhfq}YnQ#j^~4DihiZ485qCmJ$uI`|^K?Jg5z zodI-(S88EtVh#hty#$kxc9X}Tb&#NZY7ANxhD$~^5)z_3t1euyVwnRP(KOIA1T{!; z$M%It@L3xTTsF3i>lr7k1eeO-u*Ti4!kt2}%m*9mfk#ep&o|;&xoFP7@D}PH?k)bZ z(lc{P!Q~rxcOmX{fO}60j@4Sm_|gIHnMj5xNS?D%TVW{RG+`k)+`uE6xZ9REQive~ z10U2h+gpde&ktLm$6*aX+al$S;%p^WmqXnK+E_~ z4%%@5oyE?@u7~QaL9FE{xV?irRbj3}0q#vE|>F-Qtg&^jR#28NtiNIN9s{g(GZCMBBS^+<*wCt%Hu=AdfEP(;M(&EFB+JZUP&s+wt z+W?o!m5DhynZ+dxpQdlPCRL*d+U*5ew*e}}5+SXPb5Hgb%;@_H*|`Nuhzu#2Mc|Fq z#Yc|bK0Wn4s6GXiiH49<%|N?M(ROKr+T=Lb5o6uc1zyEloRe4#TL1QTE~kiQYVSF4 z8wu=c&LoI`*4^3tMzL52v>P1iYTU7nJLbSqfU(;cbSl(^WJp-&&p6Lt*WoiAoKKBF zQ-qj%Z0;mO+EXu+A>rnJAZ~iKs5WRd9V8MMz9d6tB5RHG5`d%LJu1s+JV7l#O3dC*KQXpeOudNdkPa64w`agzxCVD33=C7bCwo4qnbK$~2 z(2O*wU1ey@@I3|6cMf>JaCy{;8rY6ELqmqt;^f4f#FW%OIfwuHMpuCPOrVulhWPf1 zpogdcV^S(4E=`m}p{zPO3JuRO*CFU5|fLGrlIxq_x{#jF(w zndIW)lFEWq2Fq_HJlvI&CqmPX5d#DI`3C}^a|u94tuZ8KK>ES{pAT$^m{2ax#2B0b zY0Y5nq=^Hy@Iia!(N16xV64c1xE6Ci6HMl61|&p(9C&)d;6}GKXr(MTO0n$Ad7S}i zhhmxrlfk`?3v>pWBGO450*rNr3NZ?BMmNz405^^D98}lZH zu1(Ip8G(=~&4sLGGkkA&M(5zBCWK61E@aIQ%dMP>V^8MiAY@iTO-p`Wd6sLzY$R9j zgUVbkpYI)ZsN^$3&t<6FPRvwNJo5QrG(zTEE~Gwv?;oBK(EOeOAtROtS;6`|z2@of zbIb1_WQ_74Zo6jO@UZuQdkaFwHxH8DUZvg&$o?#Dgpff$`2~>|R&0Ts9h0#YLZhDj z3#py-Tq{aai}Dh4fZ((M!s5y#6|AdZ(|C;@Fn2b~xDTV!d`lnEk9nR)skG5&jt zZQjWi?kXuRPAw`+EsDLY&={2F{R4FF4yYt(u`oTuSi$$NBr!9mJTouFJ^GT`JME3? zz0mVRrvEc`%Guy8gr1|(<%`kIrU0GO%FV#ga67bmN%(37NzmE3Ap07{L;3!TnA}^y z1SwUHTE++4mswq(0SyCZ&~Y9Np!0M@(OaZiuLk~()AG<^mUOqx}f7}jPy#t^N_8W|3G1r33y!7P!+}y;xl+>b} z%)HcM-NZbQlXFUQ^NLG~bu;ryQj5|OlT&q*Gg6bYQ;YD}jZHZycCeVD3u;Fh8iV2q zkAcv0VDxqMp)6gHPs|AD%B|AZ)z7WcHPQoZKP*YFO3bU&Ehx&*%`8rZ#$sY|ab|iR z^pF?b#GD*FjzCfc@(ko)7?2)a@b+zxW9)1dK{{fx0y%=~Z~_$4C|Wy80Mmx`uj2dL{(i0?Kasy80kW z7qnjm9Fb|odPS*;IiM^C9-sl8p#b9OW)|xvCKc!Bl$NC8aUHr+P+9@)+(FW<3+g8m z3T4p2HK5?hOHS1VwXh8c_yuyDjlQlvgaNh#TzuhiE8Kse^a&b_0{K-Jlybm2!RJKi zrdAXr<`rk==ixC3O{KoBKAMOwC|V7{eojivNz6-5P0KZ6z!k_A5T!=)B`HCkj(%)%g_>(8j$rsM;>)ci!zC{r5M%Gpo$As z1e_fWO+hvhX(W;(^mX--c;LJS&J{$O1P^AAvtU6E&Rmcjo0$hQ9*==g2Ov4cP|pZj zT4ol5+ljil`6;D2so=s4ywA3{v;fqiDAp|~N(DEmbc-uoq43Rj7UjWmc3x&@}?BA|jOQG7>=zzl_9Uu$AEakye_QT#}ie zr<;_Smx3pRP}G4kJhDD;u0Dv;H3H4C5{LnK&j#FX zgZ6SjnM@Dtb9|u&bzv4LtHC^xRg7?Da$;_3PI6*#s%~;IzTO&&I#3!!)(5WI33(c3 zJEWt96nYQ=UC6PTppby6EKAHO1@+vDic^bLtFKr&MBx7i|DEnmCsQ-x}da;((@(C z3`DmX+_;7Hp}}bdTz7-AAh-#N#~W}pps>hHhIGTh0ZJ%+fZM;IWC~$`oeVA<(=u~% zbc+)6((y$-LJ_Dsfa}l&^&i1Dm*hj1(i1cZRUxR+grZXyd~gUTGBb;H^Gb7*@b;(S zY9MtaL_OG1;PeG5vq0zK;vXpisnFNe2T`E9%~B6s3*m_b=(HuoPHwsc0vA6_0 z#se;)!S*_UM!!I#OKHV=#U+U)$o%reoNPQML#DhyDI0vAA*4wPibgzoaQg+ca*2>% z&>aWzQn4P$nc%Lr1;}z_HO0w^c`2FrMrS~wQwTB!bW|cJDS*-<{;ogt>_l*R2tGv- zTo@AShGB6Q$l78(s2jm40bC{&mF9usH!&wC72gO9vNDickaU3q1DqQQOH)fzb-~3@ zNn$a+$V5}Aud9zH0yY$!;PFH+iZ4qNLFpBmuD}%<*nfDmr{t9B>*}ZEl!0P`V4g&b zU4k^h2QtA9C6tn2$9jY6UFZqly2hXrZ9qvVvl!G`PAsYfk8$F0FYKU5a7;qZj06|V zgn|iFtAdW61jSn_W6-gOAlvcShw3m$Ns^SA2XiSn4T0kwDwtYPk_sL+!PgGK zPzxHLfEbF8(<^xI0^1k zP+tP(Yh439&txs5Qq#zwMbemS1qnn1WL`6~sYJDU0 zfcv13=>$A3LbVcP0;v5CcY`kY7%&39LX?J}fP@!~x}YXAxDlV6lbM=VqMM(WQ)y?b zU}&aip$D!v49)c_^eVtLh#}}Wc>`SoJWfS21=K7;m`T(e2Xu%5)JcVQ*g<1jhJ@P1 z$fZ8WR-{S*+^z=K4yHzgT8@xoq(Lng@OjdRS;*p&qSVCP;?%^VGW51R2R22ECi7OOyJ#!#goGc3^1PAy6<$}cX~EiOnb&jYQBEJ)QY z2Cs>NjPn#g-C3NRUyurAmt_`bf>vr|r&bn&`qH}KGhV^FThcOfK&|MKqDtLl&?sYi zQDQ+xYHn&#W^QRwZfOx{TrRU1+{Mz(%qz}JNd@g}&@CrEt&n(HvFD=mp z$!4Zi=H#a&7H8;z2OshhOG=9pb3h>nTCkE|0GjDYEXgkdIlDj?G-X@@YKMZ2#Fqu( zia_}g&H=T4362LNtb&vqnGk!yV>95a4{`-00P(m0p$OCy$}7%<=m2+!z^35Qi|{sR zm=hiby5Q4&!3iueu{fi&B)>SLD6u3XQ@=dFC`A`EMGua#98kx#IHM#rGq0d1HK#Ns zRW}E`>J+lL1&_s`ObJm03bx7u=yDivokpm@L|6r?G2r%sJMV-%3_24S)N4vD%LCg* zNIkkYLEUxGNEnjO!NU`TrYyi|I}=3Hq9B_p`sEq=P39!Ej*^idoLA@;0 z41nl6;)yln3aGZnf#^YPm5(+Zp0-ooE6j|UgbwcACNCh6q07UUe zRQiFOn+nnh-h2Qa;vkgf;AJhS!3isOiAo)y_G)4dD2+o|p!R?fsGnYvUIotZX{p5} zcmfQr23973)PqV=13f|`JJ8eMLD>^>Ee@nCpzf_6A?JeYF>uubZeW4J6jY5N zM8E+H_5>cgp?ey^Em3d_6I>1w)>R0)5dy3W%+duPBnzr5Qj1H963bGHit)`RA{2p* zfa%aR1zpkyG6k=TLHAUE^??|=;7ck%dJ;iLrGrKZ@oEMg+^dV+Pz9TWA*u_$6$4}} z9@F49g6a{N?O;!U(u5aBd#9Vx3HbN2HAdn95fEb~? zm6!z@ol49q(>2pG1PyAHq*sCKFL1?)-pPjuCTFDPChEef20U>ExikT^`VV}2g02bZ z?rBg-her?WIt5Vc3VO2wc&-*4x_I`@3 zgxrcY@RJK(KMNn-0j<dOxff@`&$>4YZ z7c!8cN6?lx&{$|uW&wDF3N#j#3%WcKkMWQpBXBf>#{594LFG*eh8aYafynLv)db*O zR}g1`m-&Db4QO~QuLQcH8ak?~n^u&ci^rMB%0TUK$gSC6T||{?D0YIf9e7U})D4KF zmzRZsRxG+*mrnzLPtfx(E=G<1=mUi1z;bgr&bgc=$e4$O9&Jn znC3tdhc37Y4No8tF1SDfy9`@{5OTFIzL^-P(a# zq?DA@w9LFz&<4JO)S{Bi)MC&c#UxNiEV(2-wFI<7u?RFml?-Z<6j$aZ<>wTGcP-{6 z=I9pZt-aDrRsuO zclZX?uqg+%TA+JHkWB#xIiaw^NX6jp8(PW+n*=V`KoJAVtEojLmAWaZ#o(F6{32*x z*9FZ%;Rz&YG6ShXq%jZ=l)&@|MGh{z5wQ%)h$zkiM;*lBxOaY|WJ*x=4cb}^%cNiv z2u*z><$Q2~teXfe2S5P_y7dWO5JLvFbdxGeQgw4u^U_N)z-2E)7%7ny=VT_QCYB{; z=76W-KqX@eq`?O&Wk@NHGBS%nJ5#}x1;jC+-B5`+VCzBay;IW@OESw+b*oZ~@-vHdi!&07QlT!= zg=>JgD6<$`l0#)Pi*?IW6SIp_(;%_02ifCXoS2ph@ertIl$=-s769iWUC`n)h*3yh zC@IR!#VVMZmx5KixFoTt1S#O4&O{MGL<5F!ep*^_DvEwce3j-U|Z>| zA44sCoB`Q(#1>glf=7yGJSL;5gysk&z2Ia_C?kNb7Xg*q`2{Ip^@K7c>|2v_RRdSQk|6g3~!@w>F_6SnRn1)M>|>UBEsel!4JA71XUojb^YZkUAg{ zv@|(AGq0o=5}!%=6{#tR1c4`(u&Rfr0W@CP^ zcqp5Y4-kzTu+^}Z4%p3vavrE4#%x7F%)!#UBG$}2tyPdYsU@H@2a@u0kejh!GYO?r91UYo^99;{ zLdx=>CNsDyA(Y$ETHlZkKuJELMF7r;;I>p|v2IFYNh-9q#$VMUDTD++k^tDZgi?2E zu`XIa0pc_?L2!BiyAF?gz)=rL3rIl(?xcVkUZ8FRc3H4Xi83A9JV)*t!HmWt1vVEP z-=LX2=qYk|H^@WOfJzwf)w8;0dY}Vt@Q-LfhhM?H_~QJWvQ%(j!3L)Y3|E4>$>5Fw zsGWe|fVPBN5bWzh1{}c)-C#^zL&!BY1gwP?YLHaA6N_4Oo>AV>^?%KVq16w8b2kZ5544uO<=*wI79)V2a^KlFhU6k z-CR(O07)G&2?e<5-yiMgpoiKV(oEA3Nr^RuA48*&o!(n}N5Q*}!c z)A3bADC$7#agp_bqZV9)!j%=5CY2sKcV(R6DhQ-3)umRa0fP7 zaPdnhgzzSHaDh;iScz0EgEpSPvjRB236*U)+z(Et;Av^xzCfr!ocWTPmjOAC78(+u z0Vr7Zz>^Y?l|e_RV7kC1EjYD9jnpm3P6wS1h_9kWR|+nqAY%_O-Qd&(HW!b((7gi+ z3^X5tHm`%0B;haVpm+6v$_B`-J)rW8;P?e3Yk{Kyb>A^$5n6F-kuLaRAJAY17PX+h zajI@^VqRi;Y7rKBa0KEKhuK;JItd@F5+(v#Kd6tq4FP?N0Gyt$Q7L4H5+2LpYCz3Gn0oM5C_)(ldM^=ZGZy5ABJff!Lb^ah*GZrzIfS8W2%66T zYXTQ+x}|yed(+?>hd@;y=;k4!sy*0UNT60G^jajaK5#Vxs`0_?FFZj6R|C#sB^hA# zM0pm;OR$m|;z96ATd=S3*o!4&pk!ZAD1f%B!?H6X;o&nB`|$>#QViO}0-bgM?rjmO zCvcshfMgcrb`8+63ZSi)CVG(bqd_Cf$QMuGaW1klP!J+rSD|YNK3@dfJxVIdFE36l zDh6GYg*bT0)W{&MC=q-_uwGU%;?PS%r$eHt1notJpN|RF3u?*|-tmdB8Z;4t;DI+j z5E>3d_7Ujl4p(VJHzf&A@MG5iTE&6YBye#BStD;}t6%~;)dv4kV94!M-~p&Dg zfnz5nRTt(*aQBViMHAS93gisXa2{F+g0~i%8W3tFLvkzRP8QIiYvA@pBJu%jgxZbB zCct%rk6beXmsL=C#Eb`20JLL(&^d0<%eFwPFCo`%fdiUQ&4O+LxWs`T8VAY)ps61G zjU1>62seYuQ23H$(3B0Rwj#<5P^`lu+X!^{48a&dx+(|h0vk{ji*$VqXzMmOE8)oo z$an03XN?fi0~$dBI|Q<7TQ@1SBvH@Iz@a2D4|0eMSiNppW>HCLVosbPWHGfNWHq%R zzU9<7jDTAWH4;3;0rmhw1565Zv2h&Y$fP(!#G-h}nMwHk4EF)DY2elv*ej4@>_Pn@ zu;utnLcUQ65*$bu8o~X8BnH}2Pw;RO)T@?|E@}c@wFL4xsMN(GtZN23$p_Rv!f66% z=L)D81+^84^%%1Ak@cgw0$B>-5~3}^a$gk40xZJd)&RKNAlh)Gz(RH=$O0TvWLN@n zD*ByRARDlWlVLoPLkXt?&>>?4${ysjiyXzE+rU81MzRkiM3js1+&%^_9#cz7GENX z#i67qvn(+^AHTU(iOJcCDHv)>$}`h-b5nEjQ!3F-Pb@Ae0^Ku);<4o7V%_|rl++@0 z)zGmKkYCWHvWipklhFlp6N|DjOwTV$Ps{_|<&{`cQk0ogT9R6ft`DvRVj+e+$N{jx qMAr-6!;0Pc#bv2EC8?lBE=&`FD+XZ7kRu;du!5Vm;QQ(ZAprp3ojJS! literal 0 HcmV?d00001 diff --git a/site/next.config.js b/site/next.config.js index a35bfad..afbb70f 100644 --- a/site/next.config.js +++ b/site/next.config.js @@ -1,6 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "export", + headers() { + return [ + { + source: "/.well-known/apple-app-site-association", + headers: [{ key: "Content-Type", value: "application/json" }], + } + ]; + } }; module.exports = nextConfig; diff --git a/site/package.json b/site/package.json index 5deb167..4fcacc8 100644 --- a/site/package.json +++ b/site/package.json @@ -1,5 +1,5 @@ { - "name": "site", + "name": "burrow", "version": "0.1.0", "private": true, "scripts": { diff --git a/site/public/.well-known/apple-app-site-association b/site/public/.well-known/apple-app-site-association new file mode 100644 index 0000000..63262fb --- /dev/null +++ b/site/public/.well-known/apple-app-site-association @@ -0,0 +1,21 @@ +{ + "applinks": { + "details": [ + { + "appIDs": [ + "P6PV2R9443.com.hackclub.burrow" + ], + "components": [ + { + "/": "/callback/*" + } + ] + } + ] + }, + "webcredentials": { + "apps": [ + "P6PV2R9443.com.hackclub.burrow" + ] + } +} From df549d48e6d995f0efacd028f06f0d2e51595a7e Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 30 Mar 2024 16:47:59 -0700 Subject: [PATCH 13/25] Implement Slack authentication on iOS --- .github/actions/archive/action.yml | 1 - .github/actions/notarize/action.yml | 37 ++-- Apple/App/App-iOS.entitlements | 5 + Apple/App/App-macOS.entitlements | 5 + Apple/App/BurrowView.swift | 49 ++++- Apple/App/NetworkCarouselView.swift | 39 ++++ Apple/App/NetworkView.swift | 50 ----- Apple/App/OAuth2.swift | 280 +++++++++++++++++++++++++ Apple/App/TunnelButton.swift | 26 ++- Apple/Burrow.xcodeproj/project.pbxproj | 8 + 10 files changed, 419 insertions(+), 81 deletions(-) create mode 100644 Apple/App/NetworkCarouselView.swift create mode 100644 Apple/App/OAuth2.swift diff --git a/.github/actions/archive/action.yml b/.github/actions/archive/action.yml index 37282e1..e49eb0d 100644 --- a/.github/actions/archive/action.yml +++ b/.github/actions/archive/action.yml @@ -35,7 +35,6 @@ runs: -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -onlyUsePackageVersionsFromResolvedFile \ -scheme '${{ inputs.scheme }}' \ -destination '${{ inputs.destination }}' \ -archivePath '${{ inputs.archive-path }}' \ diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml index 290ed86..f3f98f2 100644 --- a/.github/actions/notarize/action.yml +++ b/.github/actions/notarize/action.yml @@ -15,10 +15,6 @@ inputs: export-path: description: The path to export the archive to required: true -outputs: - notarized-app: - description: The compressed and notarized app - value: ${{ steps.notarize.outputs.notarized-app }} runs: using: composite steps: @@ -28,31 +24,28 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"upload","method":"developer-id"}' \ + echo '{"destination":"export","method":"developer-id"}' \ | plutil -convert xml1 -o ExportOptions.plist - - xcodebuild \ - -exportArchive \ + xcodebuild -exportArchive \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -onlyUsePackageVersionsFromResolvedFile \ -authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath '${{ inputs.archive-path }}' \ + -archivePath Wallet.xcarchive \ + -exportPath Release \ -exportOptionsPlist ExportOptions.plist - until xcodebuild \ - -exportNotarizedApp \ - -allowProvisioningUpdates \ - -allowProvisioningDeviceRegistration \ - -authenticationKeyID ${{ inputs.app-store-key-id }} \ - -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ - -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath '${{ inputs.archive-path }}' \ - -exportPath ${{ inputs.export-path }} - do - echo "Failed to export app, trying again in 10s..." - sleep 10 - done + ditto -c -k --keepParent Release/Wallet.app Upload.zip + SUBMISSION_ID=$(xcrun notarytool submit --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | awk '/ id:/ { print $2; exit }') - rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist + xcrun notarytool wait $SUBMISSION_ID --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" + xcrun stapler staple Release/Wallet.app + + aa archive -a lzma -b 8m -d Release -subdir Wallet.app -o Wallet.app.aar + + rm -rf Upload.zip Release AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist diff --git a/Apple/App/App-iOS.entitlements b/Apple/App/App-iOS.entitlements index 02ee960..53fcbb7 100644 --- a/Apple/App/App-iOS.entitlements +++ b/Apple/App/App-iOS.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:burrow.rs?mode=developer + webcredentials:burrow.rs?mode=developer + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/App-macOS.entitlements b/Apple/App/App-macOS.entitlements index 02ee960..53fcbb7 100644 --- a/Apple/App/App-macOS.entitlements +++ b/Apple/App/App-macOS.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:burrow.rs?mode=developer + webcredentials:burrow.rs?mode=developer + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift index b78b1e1..8447592 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/App/BurrowView.swift @@ -1,9 +1,29 @@ +import AuthenticationServices import SwiftUI +#if !os(macOS) struct BurrowView: View { + @Environment(\.webAuthenticationSession) + private var webAuthenticationSession + var body: some View { NavigationStack { VStack { + HStack { + Text("Networks") + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + Menu { + Button("Hack Club", action: addHackClubNetwork) + Button("WireGuard", action: addWireGuardNetwork) + } label: { + Image(systemName: "plus.circle.fill") + .font(.title) + .accessibilityLabel("Add") + } + } + .padding(.top) NetworkCarouselView() Spacer() TunnelStatusView() @@ -11,9 +31,35 @@ struct BurrowView: View { .padding(.bottom) } .padding() - .navigationTitle("Networks") + .handleOAuth2Callback() } } + + private func addHackClubNetwork() { + Task { + try await authenticateWithSlack() + } + } + + private func addWireGuardNetwork() { + + } + + private func authenticateWithSlack() async throws { + guard + let authorizationEndpoint = URL(string: "https://slack.com/openid/connect/authorize"), + let tokenEndpoint = URL(string: "https://slack.com/api/openid.connect.token"), + let redirectURI = URL(string: "https://burrow.rs/callback/oauth2") else { return } + let session = OAuth2.Session( + authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint, + redirectURI: redirectURI, + scopes: ["openid", "profile"], + clientID: "2210535565.6884042183125", + clientSecret: "2793c8a5255cae38830934c664eeb62d" + ) + let response = try await session.authorize(webAuthenticationSession) + } } #if DEBUG @@ -24,3 +70,4 @@ struct NetworkView_Previews: PreviewProvider { } } #endif +#endif diff --git a/Apple/App/NetworkCarouselView.swift b/Apple/App/NetworkCarouselView.swift new file mode 100644 index 0000000..b120c60 --- /dev/null +++ b/Apple/App/NetworkCarouselView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct NetworkCarouselView: View { + var networks: [any Network] = [ + HackClub(id: "1"), + HackClub(id: "2"), + WireGuard(id: "4"), + HackClub(id: "5"), + ] + + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(networks, id: \.id) { network in + NetworkView(network: network) + .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) + .scrollTransition(.interactive, axis: .horizontal) { content, phase in + content + .scaleEffect(1.0 - abs(phase.value) * 0.1) + } + } + } + } + .scrollTargetLayout() + .scrollClipDisabled() + .scrollIndicators(.hidden) + .defaultScrollAnchor(.center) + .scrollTargetBehavior(.viewAligned) + .containerRelativeFrame(.horizontal) + } +} + +#if DEBUG +struct NetworkCarouselView_Previews: PreviewProvider { + static var previews: some View { + NetworkCarouselView() + } +} +#endif diff --git a/Apple/App/NetworkView.swift b/Apple/App/NetworkView.swift index 290254c..b839d65 100644 --- a/Apple/App/NetworkView.swift +++ b/Apple/App/NetworkView.swift @@ -30,59 +30,9 @@ struct NetworkView: View { } } -struct AddNetworkView: View { - var body: some View { - Text("Add Network") - .frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175) - .background( - RoundedRectangle(cornerRadius: 10) - .stroke(style: .init(lineWidth: 2, dash: [6])) - ) - } -} - extension NetworkView where Content == AnyView { init(network: any Network) { color = network.backgroundColor content = { AnyView(network.label) } } } - -struct NetworkCarouselView: View { - var networks: [any Network] = [ - HackClub(id: "1"), - HackClub(id: "2"), - WireGuard(id: "4"), - HackClub(id: "5"), - ] - - var body: some View { - ScrollView(.horizontal) { - LazyHStack { - ForEach(networks, id: \.id) { network in - NetworkView(network: network) - .containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center) - .scrollTransition(.interactive, axis: .horizontal) { content, phase in - content - .scaleEffect(1.0 - abs(phase.value) * 0.1) - } - } - AddNetworkView() - } - .scrollTargetLayout() - } - .scrollClipDisabled() - .scrollIndicators(.hidden) - .defaultScrollAnchor(.center) - .scrollTargetBehavior(.viewAligned) - .containerRelativeFrame(.horizontal) - } -} - -#if DEBUG -struct NetworkCarouselView_Previews: PreviewProvider { - static var previews: some View { - NetworkCarouselView() - } -} -#endif diff --git a/Apple/App/OAuth2.swift b/Apple/App/OAuth2.swift new file mode 100644 index 0000000..dc8c62b --- /dev/null +++ b/Apple/App/OAuth2.swift @@ -0,0 +1,280 @@ +import AuthenticationServices +import SwiftUI +import Foundation + +enum OAuth2 { + enum Error: Swift.Error { + case unknown + case invalidAuthorizationURL + case invalidCallbackURL + case invalidRedirectURI + } + + struct Credential { + var accessToken: String + var refreshToken: String? + var expirationDate: Date? + } + + struct Session { + var authorizationEndpoint: URL + var tokenEndpoint: URL + var redirectURI: URL + var responseType = OAuth2.ResponseType.code + var scopes: Set + var clientID: String + var clientSecret: String + + fileprivate static var queue: [Int: CheckedContinuation] = [:] + + fileprivate static func handle(url: URL) { + let continuations = queue + queue.removeAll() + for (_, continuation) in continuations { + continuation.resume(returning: url) + } + } + + public init( + authorizationEndpoint: URL, + tokenEndpoint: URL, + redirectURI: URL, + scopes: Set, + clientID: String, + clientSecret: String + ) { + self.authorizationEndpoint = authorizationEndpoint + self.tokenEndpoint = tokenEndpoint + self.redirectURI = redirectURI + self.scopes = scopes + self.clientID = clientID + self.clientSecret = clientSecret + } + + private var authorizationURL: URL { + get throws { + var queryItems: [URLQueryItem] = [ + .init(name: "client_id", value: clientID), + .init(name: "response_type", value: responseType.rawValue), + .init(name: "redirect_uri", value: redirectURI.absoluteString), + ] + if !scopes.isEmpty { + queryItems.append(.init(name: "scope", value: scopes.joined(separator: ","))) + } + guard var components = URLComponents(url: authorizationEndpoint, resolvingAgainstBaseURL: false) else { + throw OAuth2.Error.invalidAuthorizationURL + } + components.queryItems = queryItems + guard let authorizationURL = components.url else { throw OAuth2.Error.invalidAuthorizationURL } + return authorizationURL + } + } + + private func handle(callbackURL: URL) async throws -> OAuth2.AccessTokenResponse { + switch responseType { + case .code: + guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else { + throw OAuth2.Error.invalidCallbackURL + } + return try await handle(response: try components.decode(OAuth2.CodeResponse.self)) + default: + throw OAuth2.Error.invalidCallbackURL + } + } + + private func handle(response: OAuth2.CodeResponse) async throws -> OAuth2.AccessTokenResponse { + var components = URLComponents() + components.queryItems = [ + .init(name: "client_id", value: clientID), + .init(name: "client_secret", value: clientSecret), + .init(name: "grant_type", value: GrantType.authorizationCode.rawValue), + .init(name: "code", value: response.code), + .init(name: "redirect_uri", value: redirectURI.absoluteString) + ] + let httpBody = Data(components.percentEncodedQuery!.utf8) + + var request = URLRequest(url: tokenEndpoint) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = httpBody + + let session = URLSession(configuration: .ephemeral) + let (data, _) = try await session.data(for: request) + return try OAuth2.decoder.decode(OAuth2.AccessTokenResponse.self, from: data) + } + + func authorize(_ session: WebAuthenticationSession) async throws -> Credential { + let authorizationURL = try authorizationURL + let callbackURL = try await session.start( + url: authorizationURL, + redirectURI: redirectURI + ) + return try await handle(callbackURL: callbackURL).credential + } + } + + private struct CodeResponse: Codable { + var code: String + var state: String? + } + + private struct AccessTokenResponse: Codable { + var accessToken: String + var tokenType: TokenType + var expiresIn: Double? + var refreshToken: String? + + var credential: Credential { + .init(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expiresIn.map { Date.init(timeIntervalSinceNow: $0) }) + } + } + + enum TokenType: Codable, RawRepresentable { + case bearer + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "bearer": .bearer + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .bearer: "bearer" + case .unknown(let type): type + } + } + } + + enum GrantType: Codable, RawRepresentable { + case authorizationCode + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "authorization_code": .authorizationCode + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .authorizationCode: "authorization_code" + case .unknown(let type): type + } + } + } + + enum ResponseType: Codable, RawRepresentable { + case code + case idToken + case unknown(String) + + init(rawValue: String) { + self = switch rawValue.lowercased() { + case "code": .code + case "id_token": .idToken + default: .unknown(rawValue) + } + } + + var rawValue: String { + switch self { + case .code: "code" + case .idToken: "id_token" + case .unknown(let type): type + } + } + } + + fileprivate static var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } + + fileprivate static var encoder: JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return encoder + } +} + +extension WebAuthenticationSession { + func start(url: URL, redirectURI: URL) async throws -> URL { + #if canImport(BrowserEngineKit) + if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { + return try await authenticate( + using: url, + callback: try Self.callback(for: redirectURI), + additionalHeaderFields: [:] + ) + } + #endif + + return try await withThrowingTaskGroup(of: URL.self) { group in + group.addTask { + return try await authenticate(using: url, callbackURLScheme: redirectURI.scheme ?? "") + } + + let id = Int.random(in: 0.. ASWebAuthenticationSession.Callback { + switch redirectURI.scheme { + case "https": + guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } + return .https(host: host, path: redirectURI.path) + case "http": + throw OAuth2.Error.invalidRedirectURI + case .some(let scheme): + return .customScheme(scheme) + case .none: + throw OAuth2.Error.invalidRedirectURI + } + } + #endif +} + +extension View { + func handleOAuth2Callback() -> some View { + onOpenURL { url in OAuth2.Session.handle(url: url) } + } +} + +extension URLComponents { + fileprivate func decode(_ type: T.Type) throws -> T { + guard let queryItems else { + throw DecodingError.valueNotFound( + T.self, + .init(codingPath: [], debugDescription: "Missing query items") + ) + } + let data = try OAuth2.encoder.encode(try queryItems.values) + return try OAuth2.decoder.decode(T.self, from: data) + } +} + +extension Sequence where Element == URLQueryItem { + fileprivate var values: [String: String?] { + get throws { + try Dictionary(map { ($0.name, $0.value) }) { _, _ in + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Duplicate query items")) + } + } + } +} diff --git a/Apple/App/TunnelButton.swift b/Apple/App/TunnelButton.swift index df8d7e6..1f5693e 100644 --- a/Apple/App/TunnelButton.swift +++ b/Apple/App/TunnelButton.swift @@ -4,16 +4,19 @@ struct TunnelButton: View { @Environment(\.tunnel) var tunnel: any Tunnel + private var action: Action? { tunnel.action } + var body: some View { - if let action = tunnel.action { - Button { + Button { + if let action { tunnel.perform(action) - } label: { - Text(action.description) } - .padding(.horizontal) - .buttonStyle(.floating) + } label: { + Text(action.description) } + .disabled(action.isDisabled) + .padding(.horizontal) + .buttonStyle(.floating) } } @@ -40,12 +43,21 @@ extension TunnelButton { } } -extension TunnelButton.Action { +extension TunnelButton.Action? { var description: LocalizedStringKey { switch self { case .enable: "Enable" case .start: "Start" case .stop: "Stop" + case .none: "Start" + } + } + + var isDisabled: Bool { + if case .none = self { + true + } else { + false } } } diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index b08c4b0..3ea58b3 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; + D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; + D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; @@ -78,6 +80,8 @@ 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; + D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; + D000363E2BB895FB00E582EC /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -247,7 +251,9 @@ D00AA8962A4669BC005C8102 /* AppDelegate.swift */, 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, + D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */, D01A79302B81630D0024EC91 /* NetworkView.swift */, + D000363E2BB895FB00E582EC /* OAuth2.swift */, D032E64D2B8A69C90006B8AD /* Networks */, D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, @@ -476,6 +482,7 @@ 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, + D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */, D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, @@ -484,6 +491,7 @@ D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, + D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From abf1101484166ff434287056e8c0f5af727ef39e Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Mon, 22 Apr 2024 06:01:47 +0800 Subject: [PATCH 14/25] Wireguard Configuration in SQLite (#263) #241 --- .github/workflows/release-linux.yml | 34 ++++ .vscode/settings.json | 35 +++-- Apple/Burrow.xcodeproj/project.pbxproj | 24 +-- Apple/NetworkExtension/Client.swift | 60 -------- Apple/NetworkExtension/DataTypes.swift | 61 -------- .../PacketTunnelProvider.swift | 70 +++++---- Apple/NetworkExtension/libburrow/libburrow.h | 4 +- Apple/Shared/Client.swift | 106 +++++++++++++ Apple/Shared/Constants.swift | 10 ++ Apple/Shared/DataTypes.swift | 139 +++++++++++++++++ .../NWConnection+Async.swift | 0 .../NewlineProtocolFramer.swift | 0 Cargo.lock | 89 +++++++++++ Dockerfile | 26 +++- burrow-gtk/build-aux/Dockerfile | 4 +- burrow-gtk/build-aux/build_appimage.sh | 2 + burrow/Cargo.toml | 22 ++- burrow/burrow.db | Bin 0 -> 20480 bytes burrow/src/daemon/apple.rs | 14 +- burrow/src/daemon/instance.rs | 49 ++++-- burrow/src/daemon/mod.rs | 39 +++-- burrow/src/daemon/net/mod.rs | 11 +- burrow/src/daemon/net/unix.rs | 103 +++++++++---- burrow/src/daemon/rpc/mod.rs | 40 +++++ burrow/src/daemon/rpc/notification.rs | 11 ++ .../src/daemon/{command.rs => rpc/request.rs} | 9 ++ burrow/src/daemon/{ => rpc}/response.rs | 15 ++ ...equest__daemoncommand_serialization-2.snap | 5 + ...equest__daemoncommand_serialization-3.snap | 5 + ...equest__daemoncommand_serialization-4.snap | 5 + ...equest__daemoncommand_serialization-5.snap | 5 + ..._request__daemoncommand_serialization.snap | 5 + ...c__response__response_serialization-2.snap | 5 + ...c__response__response_serialization-3.snap | 5 + ...c__response__response_serialization-4.snap | 5 + ...rpc__response__response_serialization.snap | 5 + burrow/src/database.rs | 145 ++++++++++++++++++ burrow/src/lib.rs | 6 +- burrow/src/main.rs | 79 +++++----- burrow/src/wireguard/config.rs | 3 + burrow/src/wireguard/iface.rs | 36 +++-- burrow/src/wireguard/mod.rs | 2 +- tun/src/unix/apple/mod.rs | 20 +-- 43 files changed, 988 insertions(+), 325 deletions(-) create mode 100644 .github/workflows/release-linux.yml delete mode 100644 Apple/NetworkExtension/Client.swift delete mode 100644 Apple/NetworkExtension/DataTypes.swift create mode 100644 Apple/Shared/Client.swift create mode 100644 Apple/Shared/DataTypes.swift rename Apple/{NetworkExtension => Shared}/NWConnection+Async.swift (100%) rename Apple/{NetworkExtension => Shared}/NewlineProtocolFramer.swift (100%) create mode 100644 burrow/burrow.db create mode 100644 burrow/src/daemon/rpc/mod.rs create mode 100644 burrow/src/daemon/rpc/notification.rs rename burrow/src/daemon/{command.rs => rpc/request.rs} (82%) rename burrow/src/daemon/{ => rpc}/response.rs (89%) create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap create mode 100644 burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap create mode 100644 burrow/src/database.rs diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml new file mode 100644 index 0000000..6709edb --- /dev/null +++ b/.github/workflows/release-linux.yml @@ -0,0 +1,34 @@ +name: Release (Linux) +on: + release: + types: + - created +jobs: + appimage: + name: Build AppImage + runs-on: ubuntu-latest + container: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Get Build Number + id: version + shell: bash + run: | + echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT + - name: Attach Artifacts + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: builds/${{ steps.version.outputs.BUILD_NUMBER }} + overwrite: "true" + files: | + Burrow-x86_64.AppImage diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c714be..a760137 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,19 @@ { - "files.autoSave": "onFocusChange", - "files.defaultLanguage": "rust", - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "files.trimTrailingWhitespace": true, - "editor.suggest.preview": true, - "editor.acceptSuggestionOnEnter": "on", - "rust-analyzer.restartServerOnConfigChange": true, - "rust-analyzer.cargo.features": "all", - "rust-analyzer.rustfmt.extraArgs": [ - "+nightly" - ], - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer", - }, - "rust-analyzer.inlayHints.typeHints.enable": false -} \ No newline at end of file + "files.autoSave": "onFocusChange", + "files.defaultLanguage": "rust", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "editor.suggest.preview": true, + "editor.acceptSuggestionOnEnter": "on", + "rust-analyzer.restartServerOnConfigChange": true, + "rust-analyzer.cargo.features": "all", + "rust-analyzer.rustfmt.extraArgs": ["+nightly"], + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "rust-analyzer.inlayHints.typeHints.enable": false, + "rust-analyzer.linkedProjects": [ + "./burrow/Cargo.toml" + ] +} diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 3ea58b3..a3be02d 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,13 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; - 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; + 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; + 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; + 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; + 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; - D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; - D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; @@ -158,6 +158,10 @@ D00117392B30341C00D87C25 /* Shared */ = { isa = PBXGroup; children = ( + 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, + D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, + D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, + 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, D001173A2B30341C00D87C25 /* Logging.swift */, D08252752B5C9FC4005DA378 /* Constants.swift */, D00117422B30348D00D87C25 /* Shared.xcconfig */, @@ -199,10 +203,6 @@ isa = PBXGroup; children = ( D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */, - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, D020F65929E4A697002790F6 /* Info.plist */, D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */, D020F66629E4A95D002790F6 /* NetworkExtension-macOS.entitlements */, @@ -456,7 +456,11 @@ buildActionMask = 2147483647; files = ( D001173B2B30341C00D87C25 /* Logging.swift in Sources */, + 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */, D08252762B5C9FC4005DA378 /* Constants.swift in Sources */, + 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */, + 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */, + 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -464,10 +468,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D00117332B3001A400D87C25 /* NewlineProtocolFramer.swift in Sources */, - 0B28F1562ABF463A000D44B0 /* DataTypes.swift in Sources */, - D00117312B2FFFC900D87C25 /* NWConnection+Async.swift in Sources */, - 0B46E8E02AC918CA00BA2A3C /* Client.swift in Sources */, D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Apple/NetworkExtension/Client.swift b/Apple/NetworkExtension/Client.swift deleted file mode 100644 index e7c1bc8..0000000 --- a/Apple/NetworkExtension/Client.swift +++ /dev/null @@ -1,60 +0,0 @@ -import BurrowShared -import Foundation -import Network - -final class Client { - let connection: NWConnection - - private let logger = Logger.logger(for: Client.self) - private var generator = SystemRandomNumberGenerator() - - convenience init() throws { - self.init(url: try Constants.socketURL) - } - - init(url: URL) { - let endpoint: NWEndpoint - if url.isFileURL { - endpoint = .unix(path: url.path(percentEncoded: false)) - } else { - endpoint = .url(url) - } - - let parameters = NWParameters.tcp - parameters.defaultProtocolStack - .applicationProtocols - .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) - connection = NWConnection(to: endpoint, using: parameters) - connection.start(queue: .global()) - } - - func request(_ request: any Request, type: U.Type = U.self) async throws -> U { - do { - var copy = request - copy.id = generator.next(upperBound: UInt.max) - let content = try JSONEncoder().encode(copy) - logger.debug("> \(String(decoding: content, as: UTF8.self))") - - try await self.connection.send(content: content) - let (response, _, _) = try await connection.receiveMessage() - - logger.debug("< \(String(decoding: response, as: UTF8.self))") - return try JSONDecoder().decode(U.self, from: response) - } catch { - logger.error("\(error, privacy: .public)") - throw error - } - } - - deinit { - connection.cancel() - } -} - -extension Constants { - static var socketURL: URL { - get throws { - try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) - } - } -} diff --git a/Apple/NetworkExtension/DataTypes.swift b/Apple/NetworkExtension/DataTypes.swift deleted file mode 100644 index 1409fde..0000000 --- a/Apple/NetworkExtension/DataTypes.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -// swiftlint:disable identifier_name -enum BurrowError: Error { - case addrDoesntExist - case resultIsError - case cantParseResult - case resultIsNone -} - -protocol Request: Codable where Command: Codable { - associatedtype Command - - var id: UInt { get set } - var command: Command { get set } -} - -struct BurrowSingleCommand: Request { - var id: UInt - var command: String -} - -struct BurrowRequest: Request where T: Codable { - var id: UInt - var command: T -} - -struct BurrowStartRequest: Codable { - struct TunOptions: Codable { - let name: String? - let no_pi: Bool - let tun_excl: Bool - let tun_retrieve: Bool - let address: [String] - } - struct StartOptions: Codable { - let tun: TunOptions - } - let Start: StartOptions -} - -struct Response: Decodable where T: Decodable { - var id: UInt - var result: T -} - -struct BurrowResult: Codable where T: Codable { - var Ok: T? - var Err: String? -} - -struct ServerConfigData: Codable { - struct InternalConfig: Codable { - let address: [String] - let name: String? - let mtu: Int32? - } - let ServerConfig: InternalConfig -} - -// swiftlint:enable identifier_name diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index a07daa3..89e0de6 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -5,10 +5,14 @@ import os class PacketTunnelProvider: NEPacketTunnelProvider { private let logger = Logger.logger(for: PacketTunnelProvider.self) + private var client: Client? override init() { do { - libburrow.spawnInProcess(socketPath: try Constants.socketURL.path) + libburrow.spawnInProcess( + socketPath: try Constants.socketURL.path(percentEncoded: false), + dbPath: try Constants.dbURL.path(percentEncoded: false) + ) } catch { logger.error("Failed to spawn: \(error)") } @@ -17,33 +21,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func startTunnel(options: [String: NSObject]? = nil) async throws { do { let client = try Client() + self.client = client + register_events(client) - let command = BurrowRequest(id: 0, command: "ServerConfig") - let data = try await client.request(command, type: Response>.self) - - let encoded = try JSONEncoder().encode(data.result) - self.logger.log("Received final data: \(String(decoding: encoded, as: UTF8.self))") - guard let serverconfig = data.result.Ok else { - throw BurrowError.resultIsError - } - guard let tunNs = generateTunSettings(from: serverconfig) else { - throw BurrowError.addrDoesntExist - } - try await self.setTunnelNetworkSettings(tunNs) - self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - - let startRequest = BurrowRequest( - id: .random(in: (.min)..<(.max)), - command: BurrowStartRequest( - Start: BurrowStartRequest.StartOptions( - tun: BurrowStartRequest.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] - ) - ) + _ = try await self.loadTunSettings() + let startRequest = Start( + tun: Start.TunOptions( + name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] ) ) - let response = try await client.request(startRequest, type: Response>.self) - self.logger.log("Received start server response: \(String(describing: response.result))") + let response = try await client.request(startRequest, type: BurrowResult.self) + self.logger.log("Received start server response: \(String(describing: response))") } catch { self.logger.error("Failed to start tunnel: \(error)") throw error @@ -53,20 +41,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func stopTunnel(with reason: NEProviderStopReason) async { do { let client = try Client() - let command = BurrowRequest(id: 0, command: "Stop") - let data = try await client.request(command, type: Response>.self) + _ = try await client.single_request("Stop", type: BurrowResult.self) self.logger.log("Stopped client.") } catch { self.logger.error("Failed to stop tunnel: \(error)") } } - - private func generateTunSettings(from: ServerConfigData) -> NETunnelNetworkSettings? { - let cfig = from.ServerConfig + func loadTunSettings() async throws -> ServerConfig { + guard let client = self.client else { + throw BurrowError.noClient + } + let srvConfig = try await client.single_request("ServerConfig", type: BurrowResult.self) + guard let serverconfig = srvConfig.Ok else { + throw BurrowError.resultIsError + } + guard let tunNs = generateTunSettings(from: serverconfig) else { + throw BurrowError.addrDoesntExist + } + try await self.setTunnelNetworkSettings(tunNs) + self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") + return serverconfig + } + private func generateTunSettings(from: ServerConfig) -> NETunnelNetworkSettings? { + // Using a makeshift remote tunnel address let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") var v4Addresses = [String]() var v6Addresses = [String]() - for addr in cfig.address { + for addr in from.address { if IPv4Address(addr) != nil { v6Addresses.append(addr) } @@ -81,4 +82,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") return nst } + func register_events(_ client: Client) { + client.on_event(.ConfigChange) { (cfig: ServerConfig) in + self.logger.info("Config Change Notification: \(String(describing: cfig))") + self.setTunnelNetworkSettings(self.generateTunSettings(from: cfig)) + self.logger.info("Updated Tunnel Network Settings.") + } + } } diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index e500de4..2b578ab 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1,2 +1,2 @@ -__attribute__((__swift_name__("spawnInProcess(socketPath:)"))) -extern void spawn_in_process(const char * __nullable path); +__attribute__((__swift_name__("spawnInProcess(socketPath:dbPath:)"))) +extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path); diff --git a/Apple/Shared/Client.swift b/Apple/Shared/Client.swift new file mode 100644 index 0000000..f643c6c --- /dev/null +++ b/Apple/Shared/Client.swift @@ -0,0 +1,106 @@ +import Foundation +import Network + +public final class Client { + let connection: NWConnection + + private let logger = Logger.logger(for: Client.self) + private var generator = SystemRandomNumberGenerator() + private var continuations: [UInt: UnsafeContinuation] = [:] + private var eventMap: [NotificationType: [(Data) throws -> Void]] = [:] + private var task: Task? + + public convenience init() throws { + self.init(url: try Constants.socketURL) + } + + public init(url: URL) { + let endpoint: NWEndpoint + if url.isFileURL { + endpoint = .unix(path: url.path(percentEncoded: false)) + } else { + endpoint = .url(url) + } + + let parameters = NWParameters.tcp + parameters.defaultProtocolStack + .applicationProtocols + .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) + let connection = NWConnection(to: endpoint, using: parameters) + connection.start(queue: .global()) + self.connection = connection + self.task = Task { [weak self] in + while true { + let (data, _, _) = try await connection.receiveMessage() + let peek = try JSONDecoder().decode(MessagePeek.self, from: data) + switch peek.type { + case .Response: + let response = try JSONDecoder().decode(ResponsePeek.self, from: data) + self?.logger.info("Received response for \(response.id)") + guard let continuations = self?.continuations else {return} + self?.logger.debug("All keys in continuation table: \(continuations.keys)") + guard let continuation = self?.continuations[response.id] else { return } + self?.logger.debug("Got matching continuation") + continuation.resume(returning: data) + case .Notification: + let peek = try JSONDecoder().decode(NotificationPeek.self, from: data) + guard let handlers = self?.eventMap[peek.method] else { continue } + _ = try handlers.map { try $0(data) } + default: + continue + } + } + } + } + private func send(_ request: T) async throws -> U { + let data: Data = try await withUnsafeThrowingContinuation { continuation in + continuations[request.id] = continuation + do { + let data = try JSONEncoder().encode(request) + let completion: NWConnection.SendCompletion = .contentProcessed { error in + guard let error = error else { + return + } + continuation.resume(throwing: error) + } + connection.send(content: data, completion: completion) + } catch { + continuation.resume(throwing: error) + return + } + } + self.logger.debug("Got response data: \(String(describing: data.base64EncodedString()))") + let res = try JSONDecoder().decode(Response.self, from: data) + self.logger.debug("Got response data decoded: \(String(describing: res))") + return res.result + } + public func request(_ request: T, type: U.Type = U.self) async throws -> U { + let req = BurrowRequest( + id: generator.next(upperBound: UInt.max), + command: request + ) + return try await send(req) + } + public func single_request(_ request: String, type: U.Type = U.self) async throws -> U { + let req = BurrowSimpleRequest( + id: generator.next(upperBound: UInt.max), + command: request + ) + return try await send(req) + } + public func on_event(_ event: NotificationType, callable: @escaping (T) throws -> Void) { + let action = { data in + let decoded = try JSONDecoder().decode(Notification.self, from: data) + try callable(decoded.params) + } + if eventMap[event] != nil { + eventMap[event]?.append(action) + } else { + eventMap[event] = [action] + } + } + + deinit { + connection.cancel() + } +} diff --git a/Apple/Shared/Constants.swift b/Apple/Shared/Constants.swift index 634c500..a8207cd 100644 --- a/Apple/Shared/Constants.swift +++ b/Apple/Shared/Constants.swift @@ -20,4 +20,14 @@ public enum Constants { } return .success(groupContainerURL) }() + public static var socketURL: URL { + get throws { + try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) + } + } + public static var dbURL: URL { + get throws { + try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory) + } + } } diff --git a/Apple/Shared/DataTypes.swift b/Apple/Shared/DataTypes.swift new file mode 100644 index 0000000..ac49abc --- /dev/null +++ b/Apple/Shared/DataTypes.swift @@ -0,0 +1,139 @@ +import Foundation + +// swiftlint:disable identifier_name raw_value_for_camel_cased_codable_enum +public enum BurrowError: Error { + case addrDoesntExist + case resultIsError + case cantParseResult + case resultIsNone + case noClient +} + +public protocol Request: Codable where Params: Codable { + associatedtype Params + + var id: UInt { get set } + var method: String { get set } + var params: Params? { get set } +} + +public enum MessageType: String, Codable { + case Request + case Response + case Notification +} + +public struct MessagePeek: Codable { + public var type: MessageType + public init(type: MessageType) { + self.type = type + } +} + +public struct BurrowSimpleRequest: Request { + public var id: UInt + public var method: String + public var params: String? + public init(id: UInt, command: String, params: String? = nil) { + self.id = id + self.method = command + self.params = params + } +} + +public struct BurrowRequest: Request where T: Codable { + public var id: UInt + public var method: String + public var params: T? + public init(id: UInt, command: T) { + self.id = id + self.method = "\(T.self)" + self.params = command + } +} + +public struct Response: Decodable where T: Decodable { + public var id: UInt + public var result: T + public init(id: UInt, result: T) { + self.id = id + self.result = result + } +} + +public struct ResponsePeek: Codable { + public var id: UInt + public init(id: UInt) { + self.id = id + } +} + +public enum NotificationType: String, Codable { + case ConfigChange +} + +public struct Notification: Codable where T: Codable { + public var method: NotificationType + public var params: T + public init(method: NotificationType, params: T) { + self.method = method + self.params = params + } +} + +public struct NotificationPeek: Codable { + public var method: NotificationType + public init(method: NotificationType) { + self.method = method + } +} + +public struct AnyResponseData: Codable { + public var type: String + public init(type: String) { + self.type = type + } +} + +public struct BurrowResult: Codable where T: Codable { + public var Ok: T? + public var Err: String? + public init(Ok: T, Err: String? = nil) { + self.Ok = Ok + self.Err = Err + } +} + +public struct ServerConfig: Codable { + public let address: [String] + public let name: String? + public let mtu: Int32? + public init(address: [String], name: String?, mtu: Int32?) { + self.address = address + self.name = name + self.mtu = mtu + } +} + +public struct Start: Codable { + public struct TunOptions: Codable { + public let name: String? + public let no_pi: Bool + public let tun_excl: Bool + public let tun_retrieve: Bool + public let address: [String] + public init(name: String?, no_pi: Bool, tun_excl: Bool, tun_retrieve: Bool, address: [String]) { + self.name = name + self.no_pi = no_pi + self.tun_excl = tun_excl + self.tun_retrieve = tun_retrieve + self.address = address + } + } + public let tun: TunOptions + public init(tun: TunOptions) { + self.tun = tun + } +} + +// swiftlint:enable identifier_name raw_value_for_camel_cased_codable_enum diff --git a/Apple/NetworkExtension/NWConnection+Async.swift b/Apple/Shared/NWConnection+Async.swift similarity index 100% rename from Apple/NetworkExtension/NWConnection+Async.swift rename to Apple/Shared/NWConnection+Async.swift diff --git a/Apple/NetworkExtension/NewlineProtocolFramer.swift b/Apple/Shared/NewlineProtocolFramer.swift similarity index 100% rename from Apple/NetworkExtension/NewlineProtocolFramer.swift rename to Apple/Shared/NewlineProtocolFramer.swift diff --git a/Cargo.lock b/Cargo.lock index a75bd28..628e996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -47,6 +59,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "anstream" version = "0.6.11" @@ -334,6 +352,7 @@ dependencies = [ "rand", "rand_core", "ring", + "rusqlite", "schemars", "serde", "serde_json", @@ -743,6 +762,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.0.1" @@ -967,6 +998,19 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] name = "hdrhistogram" @@ -1258,6 +1302,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libsystemd" version = "0.7.0" @@ -1877,6 +1932,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.4.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2949,6 +3018,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Dockerfile b/Dockerfile index 9f54478..afd51ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN set -eux && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ - apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION && \ + apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev && \ ln -s clang-$LLVM_VERSION /usr/bin/clang && \ ln -s clang /usr/bin/clang++ && \ ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ @@ -24,12 +24,30 @@ RUN set -eux && \ apt-get remove -y --auto-remove && \ rm -rf /var/lib/apt/lists/* +ARG SQLITE_VERSION=3400100 + RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ *) exit 1 ;; \ esac && \ - rustup target add $LLVM_TARGET + rustup target add $LLVM_TARGET && \ + curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2022/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + tar xf sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + rm sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + cd sqlite-autoconf-$SQLITE_VERSION && \ + ./configure --disable-shared \ + CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ + CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ + LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ + make && \ + make install && \ + cd .. && \ + rm -rf sqlite-autoconf-$SQLITE_VERSION + +ENV SQLITE3_STATIC=1 \ + SQLITE3_INCLUDE_DIR=/usr/local/include \ + SQLITE3_LIB_DIR=/usr/local/lib ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_x86_64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ diff --git a/burrow-gtk/build-aux/Dockerfile b/burrow-gtk/build-aux/Dockerfile index df07c4a..4e71c05 100644 --- a/burrow-gtk/build-aux/Dockerfile +++ b/burrow-gtk/build-aux/Dockerfile @@ -4,7 +4,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN set -eux && \ dnf update -y && \ - dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file + dnf install -y clang ninja-build cmake meson gtk4-devel glib2-devel libadwaita-devel desktop-file-utils libappstream-glib util-linux wget fuse fuse-libs file sqlite sqlite-devel RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal ENV PATH="/root/.cargo/bin:${PATH}" @@ -12,6 +12,8 @@ ENV PATH="/root/.cargo/bin:${PATH}" WORKDIR /app COPY . /app +ENV SQLITE3_STATIC=1 + RUN cd /app/burrow-gtk/ && \ ./build-aux/build_appimage.sh diff --git a/burrow-gtk/build-aux/build_appimage.sh b/burrow-gtk/build-aux/build_appimage.sh index cd58c17..f054cd9 100755 --- a/burrow-gtk/build-aux/build_appimage.sh +++ b/burrow-gtk/build-aux/build_appimage.sh @@ -22,6 +22,8 @@ elif [ "$ARCHITECTURE" == "aarch64" ]; then chmod a+x /tmp/linuxdeploy fi + +CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET -fPIE" meson setup $BURROW_GTK_BUILD --bindir bin --prefix /usr --buildtype $BURROW_BUILD_TYPE meson compile -C $BURROW_GTK_BUILD DESTDIR=AppDir meson install -C $BURROW_GTK_BUILD diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 4e7688b..0c816f8 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,7 +10,15 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = ["rt", "macros", "sync", "io-util", "rt-multi-thread", "time", "tracing"] } +tokio = { version = "1.21", features = [ + "rt", + "macros", + "sync", + "io-util", + "rt-multi-thread", + "time", + "tracing", +] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.4", features = ["derive"] } tracing = "0.1" @@ -25,7 +33,10 @@ chacha20poly1305 = "0.10" rand = "0.8" rand_core = "0.6" aead = "0.5" -x25519-dalek = { version = "2.0", features = ["reusable_secrets", "static_secrets"] } +x25519-dalek = { version = "2.0", features = [ + "reusable_secrets", + "static_secrets", +] } ring = "0.17" parking_lot = "0.12" hmac = "0.12" @@ -37,9 +48,12 @@ async-channel = "2.1" schemars = "0.8" futures = "0.3.28" once_cell = "1.19" -console-subscriber = { version = "0.2.0" , optional = true } +console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" +[dependencies.rusqlite] +version = "0.31.0" + [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" libsystemd = "0.7" @@ -47,6 +61,7 @@ tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.27" } +rusqlite = { version = "0.31.0", features = ["bundled"] } [dev-dependencies] insta = { version = "1.32", features = ["yaml"] } @@ -62,3 +77,4 @@ pre_uninstall_script = "../package/rpm/pre_uninstall" [features] tokio-console = ["dep:console-subscriber"] +bundled = ["rusqlite/bundled"] diff --git a/burrow/burrow.db b/burrow/burrow.db new file mode 100644 index 0000000000000000000000000000000000000000..c5b6e2c614ecb4db4c264f50691b6f2cea99772a GIT binary patch literal 20480 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU=U|uU|?lH0A>aT1{MUDff0#~iz&{a zSJuG`(#R{q!1sc08t)Wd5nPH##YaP6Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONfZicc z$HFcyEzQ^%T#}fSlbV-WQl4Lw4W(F}gIpa$TopnboqSvspn?h-TnbQ-nOBlpl$MyB z8lRb>;OQ5l5ajCS8szHd>>8|4o*oaE*2qlJRPgsx2n}!n8RzU6?Cj{`3N}Wwv7Q<1 zfaXxJ1Ip9m3sO^ypcD&=1E7LbbAS%m1t7nq=A{(mXXceCgt$h8DERq@DENi?_#os9 zN|SOjljE~fD{-kv%*n|wPfdx>EGWjMq@XCZI3uwrH3e=C*nZ6bCN^HWN82kl9QqrXkB9 z2QfHiUEN)S6as=geI0`$6}(*|6&yoD{5}1ggIs-G{X!4{1#$w|{|KR+%;J*Ny!e9r zq7qOV0hxr5%q=O!6f7vpEK4j&g$EOs2uVyyDM~HI8Pq9xXi|`n2KCLkcwHFyck!3- z>+!wdTf`T`C&qh$w~N<>-uZ6SzR?gE4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R80;b7 z!o|VBz|4@U&dYEr$KN#|EG0iT)hDDP#M7y)%s3>n*efVK)xe{`($6khIH_U^2USdAr-~_TR567W$rN|LLQhA3=b(zL9R1|XAY71JH1mFA{7sYRJyiIoQ0`5rzQLB0iH+K#3bj)4Xl&R*Ju=HcZQ zhK~MaAtqSUX|Z`ug??_jc2R1Wt8borUSVowq<>&`m5YU0o{@HXWL|}#uVrO=rh!Ga zZ6hlS#2qXH?G9#$JD3OB9ZV2+Fb%LfSQyzjLFL%MIs?@IXAq!ac|B_MXb6mkz-S1J ihQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2B~hX4R^(?&G_ literal 0 HcmV?d00001 diff --git a/burrow/src/daemon/apple.rs b/burrow/src/daemon/apple.rs index 9460613..c60f131 100644 --- a/burrow/src/daemon/apple.rs +++ b/burrow/src/daemon/apple.rs @@ -18,7 +18,7 @@ static BURROW_NOTIFY: OnceCell> = OnceCell::new(); static BURROW_HANDLE: OnceCell = OnceCell::new(); #[no_mangle] -pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { +pub unsafe extern "C" fn spawn_in_process(path: *const c_char, db_path: *const c_char) { crate::tracing::initialize(); let notify = BURROW_NOTIFY.get_or_init(|| Arc::new(Notify::new())); @@ -28,6 +28,11 @@ pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { } else { Some(PathBuf::from(CStr::from_ptr(path).to_str().unwrap())) }; + let db_path_buf = if db_path.is_null() { + None + } else { + Some(PathBuf::from(CStr::from_ptr(db_path).to_str().unwrap())) + }; let sender = notify.clone(); let (handle_tx, handle_rx) = tokio::sync::oneshot::channel(); @@ -40,7 +45,12 @@ pub unsafe extern "C" fn spawn_in_process(path: *const c_char) { .unwrap(); handle_tx.send(runtime.handle().clone()).unwrap(); runtime.block_on(async { - let result = daemon_main(path_buf.as_deref(), Some(sender.clone())).await; + let result = daemon_main( + path_buf.as_deref(), + db_path_buf.as_deref(), + Some(sender.clone()), + ) + .await; if let Err(error) = result.as_ref() { error!("Burrow thread exited: {}", error); } diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 0d3e726..bc506bd 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use anyhow::Result; use tokio::{sync::RwLock, task::JoinHandle}; @@ -6,11 +9,16 @@ use tracing::{debug, info, warn}; use tun::tokio::TunInterface; use crate::{ - daemon::{ - command::DaemonCommand, - response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}, + daemon::rpc::{ + DaemonCommand, + DaemonNotification, + DaemonResponse, + DaemonResponseData, + ServerConfig, + ServerInfo, }, - wireguard::Interface, + database::{get_connection, load_interface}, + wireguard::{Config, Interface}, }; enum RunState { @@ -21,8 +29,11 @@ enum RunState { pub struct DaemonInstance { rx: async_channel::Receiver, sx: async_channel::Sender, + subx: async_channel::Sender, tun_interface: Arc>>, wg_interface: Arc>, + config: Arc>, + db_path: Option, wg_state: RunState, } @@ -30,13 +41,19 @@ impl DaemonInstance { pub fn new( rx: async_channel::Receiver, sx: async_channel::Sender, + subx: async_channel::Sender, wg_interface: Arc>, + config: Arc>, + db_path: Option<&Path>, ) -> Self { Self { rx, sx, + subx, wg_interface, tun_interface: Arc::new(RwLock::new(None)), + config, + db_path: db_path.map(|p| p.to_owned()), wg_state: RunState::Idle, } } @@ -59,24 +76,13 @@ impl DaemonInstance { self.tun_interface = self.wg_interface.read().await.get_tun(); debug!("tun_interface set: {:?}", self.tun_interface); - debug!("Cloning wg_interface"); let tmp_wg = self.wg_interface.clone(); - debug!("wg_interface cloned"); - - debug!("Spawning run task"); let run_task = tokio::spawn(async move { - debug!("Running wg_interface"); let twlock = tmp_wg.read().await; - debug!("wg_interface read lock acquired"); twlock.run().await }); - debug!("Run task spawned: {:?}", run_task); - - debug!("Setting wg_state to Running"); self.wg_state = RunState::Running(run_task); - debug!("wg_state set to Running"); - info!("Daemon started tun interface"); } } @@ -99,6 +105,17 @@ impl DaemonInstance { DaemonCommand::ServerConfig => { Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) } + DaemonCommand::ReloadConfig(interface_id) => { + let conn = get_connection(self.db_path.as_deref())?; + let cfig = load_interface(&conn, &interface_id)?; + *self.config.write().await = cfig; + self.subx + .send(DaemonNotification::ConfigChange(ServerConfig::try_from( + &self.config.read().await.to_owned(), + )?)) + .await?; + Ok(DaemonResponseData::None) + } } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 2a971dd..4469e90 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -1,40 +1,53 @@ use std::{path::Path, sync::Arc}; pub mod apple; -mod command; mod instance; mod net; -mod response; +pub mod rpc; use anyhow::Result; -pub use command::{DaemonCommand, DaemonStartOptions}; use instance::DaemonInstance; pub use net::{DaemonClient, Listener}; -pub use response::{DaemonResponse, DaemonResponseData, ServerInfo}; +pub use rpc::{DaemonCommand, DaemonResponseData, DaemonStartOptions}; use tokio::sync::{Notify, RwLock}; use tracing::{error, info}; -use crate::wireguard::{Config, Interface}; +use crate::{ + database::{get_connection, load_interface}, + wireguard::Interface, +}; -pub async fn daemon_main(path: Option<&Path>, notify_ready: Option>) -> Result<()> { +pub async fn daemon_main( + socket_path: Option<&Path>, + db_path: Option<&Path>, + notify_ready: Option>, +) -> Result<()> { let (commands_tx, commands_rx) = async_channel::unbounded(); let (response_tx, response_rx) = async_channel::unbounded(); + let (subscribe_tx, subscribe_rx) = async_channel::unbounded(); - let listener = if let Some(path) = path { + let listener = if let Some(path) = socket_path { info!("Creating listener... {:?}", path); - Listener::new_with_path(commands_tx, response_rx, path) + Listener::new_with_path(commands_tx, response_rx, subscribe_rx, path) } else { info!("Creating listener..."); - Listener::new(commands_tx, response_rx) + Listener::new(commands_tx, response_rx, subscribe_rx) }; if let Some(n) = notify_ready { n.notify_one() } let listener = listener?; - - let config = Config::default(); - let iface: Interface = config.try_into()?; - let mut instance = DaemonInstance::new(commands_rx, response_tx, Arc::new(RwLock::new(iface))); + let conn = get_connection(db_path)?; + let config = load_interface(&conn, "1")?; + let iface: Interface = config.clone().try_into()?; + let mut instance = DaemonInstance::new( + commands_rx, + response_tx, + subscribe_tx, + Arc::new(RwLock::new(iface)), + Arc::new(RwLock::new(config)), + db_path, + ); info!("Starting daemon..."); diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index fe35bae..242f479 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -1,6 +1,7 @@ -use serde::{Deserialize, Serialize}; -use super::DaemonCommand; + + + #[cfg(target_family = "unix")] mod unix; @@ -14,8 +15,4 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::{DaemonClient, Listener}; -#[derive(Clone, Serialize, Deserialize)] -pub struct DaemonRequest { - pub id: u64, - pub command: DaemonCommand, -} + diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index 26e901d..70c4207 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -10,8 +10,14 @@ use tokio::{ }; use tracing::{debug, error, info}; -use super::*; -use crate::daemon::{DaemonCommand, DaemonResponse, DaemonResponseData}; +use crate::daemon::rpc::{ + DaemonCommand, + DaemonMessage, + DaemonNotification, + DaemonRequest, + DaemonResponse, + DaemonResponseData, +}; #[cfg(not(target_vendor = "apple"))] const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; @@ -19,10 +25,17 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; #[cfg(target_vendor = "apple")] const UNIX_SOCKET_PATH: &str = "burrow.sock"; -#[derive(Debug)] +fn get_socket_path() -> String { + if std::env::var("BURROW_SOCKET_PATH").is_ok() { + return std::env::var("BURROW_SOCKET_PATH").unwrap(); + } + UNIX_SOCKET_PATH.to_string() +} + pub struct Listener { cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, inner: UnixListener, } @@ -31,9 +44,11 @@ impl Listener { pub fn new( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, ) -> Self { - let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); - Self::new_with_path(cmd_tx, rsp_rx, path)? + let socket_path = get_socket_path(); + let path = Path::new(OsStr::new(&socket_path)); + Self::new_with_path(cmd_tx, rsp_rx, sub_chan, path)? } #[throws] @@ -41,10 +56,16 @@ impl Listener { pub fn new_with_path( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, path: &Path, ) -> Self { let inner = listener_from_path_or_fd(&path, raw_fd())?; - Self { cmd_tx, rsp_rx, inner } + Self { + cmd_tx, + rsp_rx, + sub_chan, + inner, + } } #[throws] @@ -52,10 +73,16 @@ impl Listener { pub fn new_with_path( cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, + sub_chan: async_channel::Receiver, path: &Path, ) -> Self { let inner = listener_from_path(path)?; - Self { cmd_tx, rsp_rx, inner } + Self { + cmd_tx, + rsp_rx, + inner, + sub_chan, + } } pub async fn run(&self) -> Result<()> { @@ -64,9 +91,10 @@ impl Listener { let (stream, _) = self.inner.accept().await?; let cmd_tx = self.cmd_tx.clone(); let rsp_rxc = self.rsp_rx.clone(); + let sub_chan = self.sub_chan.clone(); tokio::task::spawn(async move { info!("Got connection: {:?}", stream); - Self::stream(stream, cmd_tx, rsp_rxc).await; + Self::stream(stream, cmd_tx, rsp_rxc, sub_chan).await; }); } } @@ -75,34 +103,46 @@ impl Listener { stream: UnixStream, cmd_tx: async_channel::Sender, rsp_rxc: async_channel::Receiver, + sub_chan: async_channel::Receiver, ) { let mut stream = stream; let (mut read_stream, mut write_stream) = stream.split(); let buf_reader = BufReader::new(&mut read_stream); let mut lines = buf_reader.lines(); - while let Ok(Some(line)) = lines.next_line().await { - info!("Line: {}", line); - let mut res: DaemonResponse = DaemonResponseData::None.into(); - let req = match serde_json::from_str::(&line) { - Ok(req) => Some(req), - Err(e) => { - res.result = Err(e.to_string()); - error!("Failed to parse request: {}", e); - None - } - }; - let mut res = serde_json::to_string(&res).unwrap(); - res.push('\n'); + loop { + tokio::select! { + Ok(Some(line)) = lines.next_line() => { + info!("Line: {}", line); + let mut res: DaemonResponse = DaemonResponseData::None.into(); + let req = match serde_json::from_str::(&line) { + Ok(req) => Some(req), + Err(e) => { + res.result = Err(e.to_string()); + error!("Failed to parse request: {}", e); + None + } + }; - if let Some(req) = req { - cmd_tx.send(req.command).await.unwrap(); - let res = rsp_rxc.recv().await.unwrap().with_id(req.id); - let mut retres = serde_json::to_string(&res).unwrap(); - retres.push('\n'); - info!("Sending response: {}", retres); - write_stream.write_all(retres.as_bytes()).await.unwrap(); - } else { - write_stream.write_all(res.as_bytes()).await.unwrap(); + let res = serde_json::to_string(&DaemonMessage::from(res)).unwrap(); + + if let Some(req) = req { + cmd_tx.send(req.command).await.unwrap(); + let res = rsp_rxc.recv().await.unwrap().with_id(req.id); + let mut payload = serde_json::to_string(&DaemonMessage::from(res)).unwrap(); + payload.push('\n'); + info!("Sending response: {}", payload); + write_stream.write_all(payload.as_bytes()).await.unwrap(); + } else { + write_stream.write_all(res.as_bytes()).await.unwrap(); + } + } + Ok(cmd) = sub_chan.recv() => { + info!("Got subscription command: {:?}", cmd); + let msg = DaemonMessage::from(cmd); + let mut payload = serde_json::to_string(&msg).unwrap(); + payload.push('\n'); + write_stream.write_all(payload.as_bytes()).await.unwrap(); + } } } } @@ -176,7 +216,8 @@ pub struct DaemonClient { impl DaemonClient { pub async fn new() -> Result { - let path = Path::new(OsStr::new(UNIX_SOCKET_PATH)); + let socket_path = get_socket_path(); + let path = Path::new(OsStr::new(&socket_path)); Self::new_with_path(path).await } diff --git a/burrow/src/daemon/rpc/mod.rs b/burrow/src/daemon/rpc/mod.rs new file mode 100644 index 0000000..4146e71 --- /dev/null +++ b/burrow/src/daemon/rpc/mod.rs @@ -0,0 +1,40 @@ +pub mod notification; +pub mod request; +pub mod response; + +pub use notification::DaemonNotification; +pub use request::{DaemonCommand, DaemonRequest, DaemonStartOptions}; +pub use response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}; +use serde::{Deserialize, Serialize}; + +/// The `Message` object contains either a `DaemonRequest` or a `DaemonResponse` to be serialized / deserialized +/// for our IPC communication. Our IPC protocol is based on jsonrpc (https://www.jsonrpc.org/specification#overview), +/// but deviates from it in a few ways: +/// - We differentiate Notifications from Requests explicitly. +/// - We have a "type" field to differentiate between a request, a response, and a notification. +/// - The params field may receive any json value(such as a string), not just an object or an array. +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum DaemonMessage { + Request(DaemonRequest), + Response(DaemonResponse), + Notification(DaemonNotification), +} + +impl From for DaemonMessage { + fn from(request: DaemonRequest) -> Self { + DaemonMessage::Request(request) + } +} + +impl From for DaemonMessage { + fn from(response: DaemonResponse) -> Self { + DaemonMessage::Response(response) + } +} + +impl From for DaemonMessage { + fn from(notification: DaemonNotification) -> Self { + DaemonMessage::Notification(notification) + } +} diff --git a/burrow/src/daemon/rpc/notification.rs b/burrow/src/daemon/rpc/notification.rs new file mode 100644 index 0000000..135b0e4 --- /dev/null +++ b/burrow/src/daemon/rpc/notification.rs @@ -0,0 +1,11 @@ +use rpc::ServerConfig; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::daemon::rpc; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "method", content = "params")] +pub enum DaemonNotification { + ConfigChange(ServerConfig), +} diff --git a/burrow/src/daemon/command.rs b/burrow/src/daemon/rpc/request.rs similarity index 82% rename from burrow/src/daemon/command.rs rename to burrow/src/daemon/rpc/request.rs index 53b4108..e9480aa 100644 --- a/burrow/src/daemon/command.rs +++ b/burrow/src/daemon/rpc/request.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use tun::TunOptions; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag="method", content="params")] pub enum DaemonCommand { Start(DaemonStartOptions), ServerInfo, ServerConfig, Stop, + ReloadConfig(String), } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -15,6 +17,13 @@ pub struct DaemonStartOptions { pub tun: TunOptions, } +#[derive(Clone, Serialize, Deserialize)] +pub struct DaemonRequest { + pub id: u64, + #[serde(flatten)] + pub command: DaemonCommand, +} + #[test] fn test_daemoncommand_serialization() { insta::assert_snapshot!(serde_json::to_string(&DaemonCommand::Start( diff --git a/burrow/src/daemon/response.rs b/burrow/src/daemon/rpc/response.rs similarity index 89% rename from burrow/src/daemon/response.rs rename to burrow/src/daemon/rpc/response.rs index 37ee5d9..61c9c50 100644 --- a/burrow/src/daemon/response.rs +++ b/burrow/src/daemon/rpc/response.rs @@ -2,6 +2,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tun::TunInterface; +use crate::wireguard::Config; + #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct DaemonResponse { // Error types can't be serialized, so this is the second best option. @@ -62,6 +64,18 @@ pub struct ServerConfig { pub mtu: Option, } +impl TryFrom<&Config> for ServerConfig { + type Error = anyhow::Error; + + fn try_from(config: &Config) -> anyhow::Result { + Ok(ServerConfig { + address: config.interface.address.clone(), + name: None, + mtu: config.interface.mtu.map(|mtu| mtu as i32), + }) + } +} + impl Default for ServerConfig { fn default() -> Self { Self { @@ -73,6 +87,7 @@ impl Default for ServerConfig { } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type")] pub enum DaemonResponseData { ServerInfo(ServerInfo), ServerConfig(ServerConfig), diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap new file mode 100644 index 0000000..01ec8a7 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions {\n tun: TunOptions { ..TunOptions::default() },\n })).unwrap()" +--- +{"method":"Start","params":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap new file mode 100644 index 0000000..a6a0466 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::ServerInfo).unwrap()" +--- +{"method":"ServerInfo"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap new file mode 100644 index 0000000..f930051 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Stop).unwrap()" +--- +{"method":"Stop"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap new file mode 100644 index 0000000..89dc42c --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization-5.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::ServerConfig).unwrap()" +--- +{"method":"ServerConfig"} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap new file mode 100644 index 0000000..aeca659 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__request__daemoncommand_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/request.rs +expression: "serde_json::to_string(&DaemonCommand::Start(DaemonStartOptions::default())).unwrap()" +--- +{"method":"Start","params":{"tun":{"name":null,"no_pi":false,"tun_excl":false,"tun_retrieve":false,"address":[]}}} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap new file mode 100644 index 0000000..d7bd712 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-2.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerInfo(ServerInfo {\n name: Some(\"burrow\".to_string()),\n ip: None,\n mtu: Some(1500),\n }))))?" +--- +{"result":{"Ok":{"type":"ServerInfo","name":"burrow","ip":null,"mtu":1500}},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap new file mode 100644 index 0000000..30068f3 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-3.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Err::(\"error\".to_string())))?" +--- +{"result":{"Err":"error"},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap new file mode 100644 index 0000000..c40db25 --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" +--- +{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"name":null,"mtu":null}},"id":0} diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap new file mode 100644 index 0000000..31bd84b --- /dev/null +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization.snap @@ -0,0 +1,5 @@ +--- +source: burrow/src/daemon/rpc/response.rs +expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::None)))?" +--- +{"result":{"Ok":{"type":"None"}},"id":0} diff --git a/burrow/src/database.rs b/burrow/src/database.rs new file mode 100644 index 0000000..0047b01 --- /dev/null +++ b/burrow/src/database.rs @@ -0,0 +1,145 @@ +use std::path::Path; + +use anyhow::Result; +use rusqlite::{params, Connection}; + +use crate::wireguard::config::{Config, Interface, Peer}; + +#[cfg(target_vendor = "apple")] +const DB_PATH: &str = "burrow.db"; + +#[cfg(not(target_vendor = "apple"))] +const DB_PATH: &str = "/var/lib/burrow/burrow.db"; + +const CREATE_WG_INTERFACE_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_interface ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + listen_port INTEGER, + mtu INTEGER, + private_key TEXT NOT NULL, + address TEXT NOT NULL, + dns TEXT NOT NULL +)"; + +const CREATE_WG_PEER_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_peer ( + interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE, + endpoint TEXT NOT NULL, + public_key TEXT NOT NULL, + allowed_ips TEXT NOT NULL, + preshared_key TEXT +)"; + +const CREATE_NETWORK_TABLE: &str = "CREATE TABLE IF NOT EXISTS network ( + interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE +)"; + +pub fn initialize_tables(conn: &Connection) -> Result<()> { + conn.execute(CREATE_WG_INTERFACE_TABLE, [])?; + conn.execute(CREATE_WG_PEER_TABLE, [])?; + conn.execute(CREATE_NETWORK_TABLE, [])?; + Ok(()) +} + +fn parse_lst(s: &str) -> Vec { + if s.is_empty() { + return vec![]; + } + s.split(',').map(|s| s.to_string()).collect() +} + +fn to_lst(v: &Vec) -> String { + v.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") +} + +pub fn load_interface(conn: &Connection, interface_id: &str) -> Result { + let iface = conn.query_row( + "SELECT private_key, dns, address, listen_port, mtu FROM wg_interface WHERE id = ?", + [&interface_id], + |row| { + let dns_rw: String = row.get(1)?; + let dns = parse_lst(&dns_rw); + let address_rw: String = row.get(2)?; + let address = parse_lst(&address_rw); + Ok(Interface { + private_key: row.get(0)?, + dns, + address, + mtu: row.get(4)?, + listen_port: row.get(3)?, + }) + }, + )?; + let mut peers_stmt = conn.prepare("SELECT public_key, preshared_key, allowed_ips, endpoint FROM wg_peer WHERE interface_id = ?")?; + let peers = peers_stmt + .query_map([&interface_id], |row| { + let preshared_key: Option = row.get(1)?; + let allowed_ips_rw: String = row.get(2)?; + let allowed_ips: Vec = + allowed_ips_rw.split(',').map(|s| s.to_string()).collect(); + Ok(Peer { + public_key: row.get(0)?, + preshared_key, + allowed_ips, + endpoint: row.get(3)?, + persistent_keepalive: None, + name: None, + }) + })? + .collect::>>()?; + Ok(Config { interface: iface, peers }) +} + +pub fn dump_interface(conn: &Connection, config: &Config) -> Result<()> { + let mut stmt = conn.prepare("INSERT INTO wg_interface (private_key, dns, address, listen_port, mtu) VALUES (?, ?, ?, ?, ?)")?; + let cif = &config.interface; + stmt.execute(params![ + cif.private_key, + to_lst(&cif.dns), + to_lst(&cif.address), + cif.listen_port, + cif.mtu + ])?; + let interface_id = conn.last_insert_rowid(); + let mut stmt = conn.prepare("INSERT INTO wg_peer (interface_id, public_key, preshared_key, allowed_ips, endpoint) VALUES (?, ?, ?, ?, ?)")?; + for peer in &config.peers { + stmt.execute(params![ + &interface_id, + &peer.public_key, + &peer.preshared_key, + &peer.allowed_ips.join(","), + &peer.endpoint + ])?; + } + Ok(()) +} + +pub fn get_connection(path: Option<&Path>) -> Result { + let p = path.unwrap_or_else(|| std::path::Path::new(DB_PATH)); + if !p.exists() { + let conn = Connection::open(p)?; + initialize_tables(&conn)?; + dump_interface(&conn, &Config::default())?; + return Ok(conn); + } + Ok(Connection::open(p)?) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + #[test] + fn test_db() { + let conn = Connection::open_in_memory().unwrap(); + initialize_tables(&conn).unwrap(); + let config = Config::default(); + dump_interface(&conn, &config).unwrap(); + let loaded = load_interface(&conn, "1").unwrap(); + assert_eq!(config, loaded); + } +} diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index c5406b2..d9ebf7e 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -3,16 +3,18 @@ pub mod wireguard; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +pub mod database; pub(crate) mod tracing; #[cfg(target_vendor = "apple")] pub use daemon::apple::spawn_in_process; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub use daemon::{ + rpc::DaemonResponse, + rpc::ServerInfo, DaemonClient, DaemonCommand, - DaemonResponse, DaemonResponseData, DaemonStartOptions, - ServerInfo, }; diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 71d1c02..295373a 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -14,6 +14,9 @@ use tun::TunOptions; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use crate::daemon::DaemonResponseData; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +pub mod database; + #[derive(Parser)] #[command(name = "Burrow")] #[command(author = "Hack Club ")] @@ -42,6 +45,14 @@ enum Commands { ServerInfo, /// Server config ServerConfig, + /// Reload Config + ReloadConfig(ReloadConfigArgs), +} + +#[derive(Args)] +struct ReloadConfigArgs { + #[clap(long, short)] + interface_id: String, } #[derive(Args)] @@ -69,13 +80,8 @@ async fn try_stop() -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -async fn try_serverinfo() -> Result<()> { - let mut client = DaemonClient::new().await?; - let res = client.send_command(DaemonCommand::ServerInfo).await?; - match res.result { - Ok(DaemonResponseData::ServerInfo(si)) => { - println!("Got Result! {:?}", si); - } +fn handle_unexpected(res: Result) { + match res { Ok(DaemonResponseData::None) => { println!("Server not started.") } @@ -86,6 +92,17 @@ async fn try_serverinfo() -> Result<()> { println!("Error when retrieving from server: {}", e) } } +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverinfo() -> Result<()> { + let mut client = DaemonClient::new().await?; + let res = client.send_command(DaemonCommand::ServerInfo).await?; + if let Ok(DaemonResponseData::ServerInfo(si)) = res.result { + println!("Got Result! {:?}", si); + } else { + handle_unexpected(res.result); + } Ok(()) } @@ -93,40 +110,25 @@ async fn try_serverinfo() -> Result<()> { async fn try_serverconfig() -> Result<()> { let mut client = DaemonClient::new().await?; let res = client.send_command(DaemonCommand::ServerConfig).await?; - match res.result { - Ok(DaemonResponseData::ServerConfig(cfig)) => { - println!("Got Result! {:?}", cfig); - } - Ok(DaemonResponseData::None) => { - println!("Server not started.") - } - Ok(res) => { - println!("Unexpected Response: {:?}", res) - } - Err(e) => { - println!("Error when retrieving from server: {}", e) - } + if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result { + println!("Got Result! {:?}", cfig); + } else { + handle_unexpected(res.result); } Ok(()) } -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_start() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_stop() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_serverinfo() -> Result<()> { - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_vendor = "apple")))] -async fn try_serverconfig() -> Result<()> { +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_reloadconfig(interface_id: String) -> Result<()> { + let mut client = DaemonClient::new().await?; + let res = client + .send_command(DaemonCommand::ReloadConfig(interface_id)) + .await?; + if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result { + println!("Got Result! {:?}", cfig); + } else { + handle_unexpected(res.result); + } Ok(()) } @@ -139,9 +141,10 @@ async fn main() -> Result<()> { match &cli.command { Commands::Start(..) => try_start().await?, Commands::Stop => try_stop().await?, - Commands::Daemon(_) => daemon::daemon_main(None, None).await?, + Commands::Daemon(_) => daemon::daemon_main(None, None, None).await?, Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, + Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, } Ok(()) diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index ed7b3cd..bd86a9f 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -31,6 +31,7 @@ fn parse_public_key(string: &str) -> PublicKey { /// A raw version of Peer Config that can be used later to reflect configuration files. /// This should be later converted to a `WgPeer`. /// Refers to https://github.com/pirate/wireguard-docs?tab=readme-ov-file#overview +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Peer { pub public_key: String, pub preshared_key: Option, @@ -40,6 +41,7 @@ pub struct Peer { pub name: Option, } +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Interface { pub private_key: String, pub address: Vec, @@ -48,6 +50,7 @@ pub struct Interface { pub mtu: Option, } +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Config { pub peers: Vec, pub interface: Interface, // Support for multiple interfaces? diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 6097082..84b5489 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -1,21 +1,26 @@ -use std::{net::IpAddr, sync::Arc}; -use std::ops::Deref; +use std::{net::IpAddr, ops::Deref, sync::Arc}; use anyhow::Error; use fehler::throws; use futures::future::join_all; use ip_network_table::IpNetworkTable; -use tokio::sync::{RwLock, Notify}; +use tokio::sync::{Notify, RwLock}; use tracing::{debug, error}; use tun::tokio::TunInterface; use super::{noise::Tunnel, Peer, PeerPcb}; -struct IndexedPcbs { +pub struct IndexedPcbs { pcbs: Vec>, allowed_ips: IpNetworkTable, } +impl Default for IndexedPcbs { + fn default() -> Self { + Self::new() + } +} + impl IndexedPcbs { pub fn new() -> Self { Self { @@ -49,12 +54,12 @@ impl FromIterator for IndexedPcbs { enum IfaceStatus { Running, - Idle + Idle, } pub struct Interface { - tun: Arc>>, - pcbs: Arc, + pub tun: Arc>>, + pub pcbs: Arc, status: Arc>, stop_notifier: Arc, } @@ -73,7 +78,12 @@ impl Interface { .collect::>()?; let pcbs = Arc::new(pcbs); - Self { pcbs, tun: Arc::new(RwLock::new(None)), status: Arc::new(RwLock::new(IfaceStatus::Idle)), stop_notifier: Arc::new(Notify::new()) } + Self { + pcbs, + tun: Arc::new(RwLock::new(None)), + status: Arc::new(RwLock::new(IfaceStatus::Idle)), + stop_notifier: Arc::new(Notify::new()), + } } pub async fn set_tun(&self, tun: TunInterface) { @@ -87,7 +97,7 @@ impl Interface { self.tun.clone() } - pub async fn remove_tun(&self){ + pub async fn remove_tun(&self) { let mut st = self.status.write().await; self.stop_notifier.notify_waiters(); *st = IfaceStatus::Idle; @@ -95,9 +105,7 @@ impl Interface { pub async fn run(&self) -> anyhow::Result<()> { let pcbs = self.pcbs.clone(); - let tun = self - .tun - .clone(); + let tun = self.tun.clone(); let status = self.status.clone(); let stop_notifier = self.stop_notifier.clone(); log::info!("Starting interface"); @@ -153,9 +161,7 @@ impl Interface { }; let mut tsks = vec![]; - let tun = self - .tun - .clone(); + let tun = self.tun.clone(); let outgoing = tokio::task::spawn(outgoing); tsks.push(outgoing); debug!("preparing to spawn read tasks"); diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index 15563fb..4c70a7f 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -1,4 +1,4 @@ -mod config; +pub mod config; mod iface; mod noise; mod pcb; diff --git a/tun/src/unix/apple/mod.rs b/tun/src/unix/apple/mod.rs index 6e859ca..74e93eb 100644 --- a/tun/src/unix/apple/mod.rs +++ b/tun/src/unix/apple/mod.rs @@ -1,11 +1,13 @@ -use std::{io::{Error, IoSlice}, mem, net::{Ipv4Addr, SocketAddrV4}, os::fd::{AsRawFd, FromRawFd, RawFd}, ptr}; -use std::net::{IpAddr, Ipv6Addr, SocketAddrV6}; -use std::ptr::addr_of; +use std::{ + io::{Error, IoSlice}, + mem, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4}, + os::fd::{AsRawFd, FromRawFd, RawFd}, +}; use byteorder::{ByteOrder, NetworkEndian}; use fehler::throws; -use libc::{c_char, iovec, writev, AF_INET, AF_INET6, sockaddr_in6}; -use nix::sys::socket::SockaddrIn6; +use libc::{c_char, iovec, writev, AF_INET, AF_INET6}; use socket2::{Domain, SockAddr, Socket, Type}; use tracing::{self, instrument}; @@ -69,11 +71,11 @@ impl TunInterface { #[throws] fn configure(&self, options: TunOptions) { - for addr in options.address{ + for addr in options.address { if let Ok(addr) = addr.parse::() { match addr { - IpAddr::V4(addr) => {self.set_ipv4_addr(addr)?} - IpAddr::V6(addr) => {self.set_ipv6_addr(addr)?} + IpAddr::V4(addr) => self.set_ipv4_addr(addr)?, + IpAddr::V6(addr) => self.set_ipv6_addr(addr)?, } } } @@ -146,7 +148,7 @@ impl TunInterface { } #[throws] - pub fn set_ipv6_addr(&self, addr: Ipv6Addr) { + pub fn set_ipv6_addr(&self, _addr: Ipv6Addr) { // let addr = SockAddr::from(SocketAddrV6::new(addr, 0, 0, 0)); // println!("addr: {:?}", addr); // let mut iff = self.in6_ifreq()?; From bca07c33b8bf9ef44f4a57e7a1fa6fdc15ba4790 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 25 May 2024 09:06:53 -0700 Subject: [PATCH 15/25] Start authentication flow --- .gitignore | 1 + .../NetworkExtension/libburrow/build-rust.sh | 2 +- Cargo.lock | 1153 +++++++++++------ Cargo.toml | 5 + Dockerfile | 55 +- burrow/Cargo.toml | 15 +- burrow/src/auth/client.rs | 24 + burrow/src/auth/mod.rs | 2 + burrow/src/auth/server/db.rs | 89 ++ burrow/src/auth/server/mod.rs | 62 + burrow/src/auth/server/providers/mod.rs | 8 + burrow/src/auth/server/providers/slack.rs | 102 ++ burrow/src/lib.rs | 2 + burrow/src/main.rs | 12 +- tun/Cargo.toml | 9 +- 15 files changed, 1104 insertions(+), 437 deletions(-) create mode 100644 burrow/src/auth/client.rs create mode 100644 burrow/src/auth/mod.rs create mode 100644 burrow/src/auth/server/db.rs create mode 100644 burrow/src/auth/server/mod.rs create mode 100644 burrow/src/auth/server/providers/mod.rs create mode 100644 burrow/src/auth/server/providers/slack.rs diff --git a/.gitignore b/.gitignore index dc886ed..96b2507 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ xcuserdata # Rust target/ +.env .DS_STORE .idea/ diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index fffa0d0..e7204a5 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -70,7 +70,7 @@ fi # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" diff --git a/Cargo.lock b/Cargo.lock index 628e996..ce263f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", @@ -52,62 +52,57 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -115,18 +110,17 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "async-channel" -version = "2.1.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -151,25 +145,25 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -178,13 +172,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -193,12 +187,46 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", ] +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -208,8 +236,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -217,10 +245,31 @@ dependencies = [ ] [[package]] -name = "backtrace" -version = "0.3.69" +name = "axum-core" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -237,6 +286,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -284,7 +339,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.48", + "syn 2.0.68", "which", ] @@ -296,9 +351,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake2" @@ -320,9 +375,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "burrow" @@ -331,13 +386,15 @@ dependencies = [ "aead", "anyhow", "async-channel", - "base64", + "axum 0.7.5", + "base64 0.21.7", "blake2", "caps", "chacha20poly1305", "clap", "console", "console-subscriber", + "dotenv", "fehler", "futures", "hmac", @@ -351,6 +408,7 @@ dependencies = [ "parking_lot", "rand", "rand_core", + "reqwest 0.12.5", "ring", "rusqlite", "schemars", @@ -374,9 +432,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bzip2" @@ -411,12 +469,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -471,20 +530,20 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.1", + "libloading 0.8.4", ] [[package]] name = "clap" -version = "4.4.18" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -492,9 +551,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -504,33 +563,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -618,27 +677,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -653,15 +712,14 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", - "platforms", "rustc_version", "subtle", "zeroize", @@ -675,7 +733,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -699,16 +757,22 @@ dependencies = [ ] [[package]] -name = "dyn-clone" -version = "1.0.16" +name = "dotenv" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -718,9 +782,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -733,9 +797,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -743,9 +807,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -754,9 +818,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener", "pin-project-lite", @@ -776,9 +840,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fehler" @@ -802,15 +866,15 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -902,7 +966,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -947,9 +1011,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -958,9 +1022,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -970,17 +1034,17 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 2.1.0", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -995,21 +1059,20 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1018,7 +1081,7 @@ version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64", + "base64 0.21.7", "byteorder", "flate2", "nom", @@ -1027,15 +1090,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1063,9 +1126,20 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1079,15 +1153,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1103,17 +1200,17 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1125,13 +1222,51 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.0", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1144,12 +1279,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.29", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.4.0", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "idna" version = "0.5.0" @@ -1172,12 +1327,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1191,16 +1346,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.34.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" dependencies = [ "console", "lazy_static", "linked-hash-map", "serde", "similar", - "yaml-rust", ] [[package]] @@ -1232,43 +1386,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] -name = "itertools" -version = "0.11.0" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -1278,9 +1438,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1294,12 +1454,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.6", ] [[package]] @@ -1339,15 +1499,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1355,9 +1515,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchers" @@ -1376,9 +1536,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -1391,9 +1551,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1418,7 +1578,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -1435,18 +1595,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1455,11 +1615,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1490,10 +1649,10 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "libc", - "memoffset 0.9.0", + "memoffset 0.9.1", ] [[package]] @@ -1517,10 +1676,16 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1537,9 +1702,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -1552,17 +1717,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -1579,7 +1744,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -1590,9 +1755,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1614,9 +1779,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1624,15 +1789,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1672,29 +1837,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1704,15 +1869,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" - -[[package]] -name = "platforms" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "poly1305" @@ -1739,28 +1898,28 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -1768,31 +1927,78 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] [[package]] -name = "quote" -version = "1.0.35" +name = "quinn" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1829,23 +2035,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.5", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1859,13 +2065,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -1876,25 +2082,25 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "hyper-tls", "ipnet", "js-sys", @@ -1904,9 +2110,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1915,21 +2123,64 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg 0.52.0", ] [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1938,7 +2189,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1948,9 +2199,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -1969,11 +2220,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1981,16 +2232,66 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.14" +name = "rustls" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" @@ -2003,9 +2304,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -2015,14 +2316,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -2033,11 +2334,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2046,9 +2347,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2056,52 +2357,62 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.112" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2163,10 +2474,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "similar" -version = "2.4.0" +name = "signal-hook-registry" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "slab" @@ -2179,18 +2499,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2205,7 +2525,7 @@ version = "9.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" dependencies = [ - "base64", + "base64 0.21.7", "digest", "hex", "miette", @@ -2217,15 +2537,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2240,9 +2560,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -2255,6 +2575,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2278,42 +2604,41 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2321,11 +2646,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "num-conv", "powerfmt", "serde", "time-core", @@ -2339,9 +2665,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" dependencies = [ "tinyvec_macros", ] @@ -2354,9 +2680,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2364,6 +2690,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "tracing", @@ -2382,13 +2709,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -2402,10 +2729,21 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.14" +name = "tokio-rustls" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2414,16 +2752,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -2434,13 +2771,13 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", - "base64", + "axum 0.6.20", + "base64 0.21.7", "bytes", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -2491,6 +2828,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2504,7 +2842,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -2603,7 +2941,7 @@ dependencies = [ "libloading 0.7.4", "log", "nix 0.26.4", - "reqwest", + "reqwest 0.11.27", "schemars", "serde", "socket2", @@ -2636,18 +2974,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "universal-hash" @@ -2667,9 +3005,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2678,15 +3016,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "serde", ] @@ -2726,9 +3064,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2736,24 +3074,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2763,9 +3101,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2773,33 +3111,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -2814,9 +3161,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -2864,7 +3211,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -2884,17 +3231,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 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", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2905,9 +3253,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2917,9 +3265,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2929,9 +3277,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2941,9 +3295,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2953,9 +3307,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2965,9 +3319,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2977,9 +3331,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winreg" @@ -2992,10 +3346,20 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.0" +name = "winreg" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", @@ -3005,44 +3369,35 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] +checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -3055,7 +3410,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.68", ] [[package]] @@ -3099,9 +3454,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 44981a2..362ba2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,8 @@ members = ["burrow", "tun"] resolver = "2" exclude = ["burrow-gtk"] + +[profile.release] +lto = true +panic = "abort" +opt-level = "z" diff --git a/Dockerfile b/Dockerfile index afd51ea..e55eb58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.76.0-slim-bookworm AS builder +FROM docker.io/library/rust:1.77-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 @@ -8,7 +8,7 @@ ENV KEYRINGS /etc/apt/keyrings RUN set -eux && \ mkdir -p $KEYRINGS && \ apt-get update && \ - apt-get install --no-install-recommends -y gpg curl musl-dev && \ + apt-get install --no-install-recommends -y gpg curl busybox make musl-dev && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ @@ -24,30 +24,31 @@ RUN set -eux && \ apt-get remove -y --auto-remove && \ rm -rf /var/lib/apt/lists/* -ARG SQLITE_VERSION=3400100 +RUN case $TARGETPLATFORM in \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ + esac && \ + rustup target add $LLVM_TARGET + +ARG SQLITE_VERSION=3460000 RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ - *) exit 1 ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl MUSL_TARGET=aarch64-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl MUSL_TARGET=x86_64-linux-musl ;; \ + *) exit 1 ;; \ esac && \ - rustup target add $LLVM_TARGET && \ - curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2022/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ + curl --proto '=https' --tlsv1.2 -sSfO https://www.sqlite.org/2024/sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ tar xf sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ - rm sqlite-autoconf-$SQLITE_VERSION.tar.gz && \ cd sqlite-autoconf-$SQLITE_VERSION && \ - ./configure --disable-shared \ - CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ - CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ - LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ + ./configure --disable-shared --disable-dependency-tracking \ + CC="clang-$LLVM_VERSION -target $LLVM_TARGET" \ + CFLAGS="-I/usr/local/include -I/usr/include/$MUSL_TARGET" \ + LDFLAGS="-L/usr/local/lib -L/usr/lib/$MUSL_TARGET -L/lib/$MUSL_TARGET" && \ make && \ make install && \ cd .. && \ - rm -rf sqlite-autoconf-$SQLITE_VERSION - -ENV SQLITE3_STATIC=1 \ - SQLITE3_INCLUDE_DIR=/usr/local/include \ - SQLITE3_LIB_DIR=/usr/local/lib + rm -rf sqlite-autoconf-$SQLITE_VERSION sqlite-autoconf-$SQLITE_VERSION.tar.gz ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_x86_64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ @@ -55,14 +56,17 @@ ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_aarch64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/x86_64-linux-musl -L/lib/x86_64-linux-musl -C linker=rust-lld" \ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/aarch64-linux-musl -L/lib/aarch64-linux-musl -C linker=rust-lld" \ - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse + CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ + SQLITE3_STATIC=1 \ + SQLITE3_INCLUDE_DIR=/usr/local/include \ + SQLITE3_LIB_DIR=/usr/local/lib COPY . . RUN case $TARGETPLATFORM in \ - "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ - "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ - *) exit 1 ;; \ + "linux/arm64") LLVM_TARGET=aarch64-unknown-linux-musl ;; \ + "linux/amd64") LLVM_TARGET=x86_64-unknown-linux-musl ;; \ + *) exit 1 ;; \ esac && \ cargo install --path burrow --target $LLVM_TARGET @@ -71,7 +75,8 @@ WORKDIR /tmp/rootfs RUN set -eux && \ mkdir -p ./bin ./etc ./tmp ./data && \ mv /usr/local/cargo/bin/burrow ./bin/burrow && \ - echo 'burrow:x:10001:10001::/tmp:/sbin/nologin' > ./etc/passwd && \ + cp /bin/busybox ./bin/busybox && \ + echo 'burrow:x:10001:10001::/tmp:/bin/busybox' > ./etc/passwd && \ echo 'burrow:x:10001:' > ./etc/group && \ chown -R 10001:10001 ./tmp ./data && \ chmod 0777 ./tmp @@ -90,4 +95,6 @@ USER 10001:10001 COPY --from=builder /tmp/rootfs / WORKDIR /data -ENTRYPOINT ["/bin/burrow"] +EXPOSE 8080 + +CMD ["/bin/burrow", "auth-server"] diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 0c816f8..0fb63a5 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -10,12 +10,13 @@ crate-type = ["lib", "staticlib"] [dependencies] anyhow = "1.0" -tokio = { version = "1.21", features = [ +tokio = { version = "1.37", features = [ "rt", "macros", "sync", "io-util", "rt-multi-thread", + "signal", "time", "tracing", ] } @@ -24,7 +25,7 @@ clap = { version = "4.4", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" tracing-oslog = { git = "https://github.com/Stormshield-robinc/tracing-oslog" } -tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"] } +tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1.0" @@ -50,9 +51,13 @@ futures = "0.3.28" once_cell = "1.19" console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" - -[dependencies.rusqlite] -version = "0.31.0" +axum = "0.7.4" +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } +rusqlite = "0.31.0" +dotenv = "0.15.0" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" diff --git a/burrow/src/auth/client.rs b/burrow/src/auth/client.rs new file mode 100644 index 0000000..e9721f3 --- /dev/null +++ b/burrow/src/auth/client.rs @@ -0,0 +1,24 @@ +use std::env::var; + +use anyhow::Result; +use reqwest::Url; + +pub async fn login() -> Result<()> { + let state = "vt :P"; + let nonce = "no"; + + let mut url = Url::parse("https://slack.com/openid/connect/authorize")?; + let mut q = url.query_pairs_mut(); + q.append_pair("response_type", "code"); + q.append_pair("scope", "openid profile email"); + q.append_pair("client_id", &var("CLIENT_ID")?); + q.append_pair("state", state); + q.append_pair("team", &var("SLACK_TEAM_ID")?); + q.append_pair("nonce", nonce); + q.append_pair("redirect_uri", "https://burrow.rs/callback"); + drop(q); + + println!("Continue auth in your browser:\n{}", url.as_str()); + + Ok(()) +} diff --git a/burrow/src/auth/mod.rs b/burrow/src/auth/mod.rs new file mode 100644 index 0000000..c07f47e --- /dev/null +++ b/burrow/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/burrow/src/auth/server/db.rs b/burrow/src/auth/server/db.rs new file mode 100644 index 0000000..b74f7ce --- /dev/null +++ b/burrow/src/auth/server/db.rs @@ -0,0 +1,89 @@ +use anyhow::Result; + +pub static PATH: &str = "./server.sqlite3"; + +pub fn init_db() -> Result<()> { + let conn = rusqlite::Connection::open(PATH)?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS user ( + id PRIMARY KEY, + created_at TEXT NOT NULL + )", + (), + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS user_connection ( + user_id INTEGER REFERENCES user(id) ON DELETE CASCADE, + openid_provider TEXT NOT NULL, + openid_user_id TEXT NOT NULL, + openid_user_name TEXT NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + PRIMARY KEY (openid_provider, openid_user_id) + )", + (), + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS device ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + public_key TEXT NOT NULL, + apns_token TEXT UNIQUE, + user_id INT REFERENCES user(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (datetime('now')) CHECK(created_at IS datetime(created_at)), + ipv4 TEXT NOT NULL UNIQUE, + ipv6 TEXT NOT NULL UNIQUE, + access_token TEXT NOT NULL UNIQUE, + refresh_token TEXT NOT NULL UNIQUE, + expires_at TEXT NOT NULL DEFAULT (datetime('now', '+7 days')) CHECK(expires_at IS datetime(expires_at)) + )", + () + ).unwrap(); + + Ok(()) +} + +pub fn store_connection( + openid_user: super::providers::OpenIdUser, + openid_provider: &str, + access_token: &str, + refresh_token: Option<&str>, +) -> Result<()> { + log::debug!("Storing openid user {:#?}", openid_user); + let conn = rusqlite::Connection::open(PATH)?; + + conn.execute( + "INSERT OR IGNORE INTO user (id, created_at) VALUES (?, datetime('now'))", + (&openid_user.sub,), + )?; + conn.execute( + "INSERT INTO user_connection (user_id, openid_provider, openid_user_id, openid_user_name, access_token, refresh_token) VALUES ( + (SELECT id FROM user WHERE id = ?), + ?, + ?, + ?, + ?, + ? + )", + (&openid_user.sub, &openid_provider, &openid_user.sub, &openid_user.name, access_token, refresh_token), + )?; + + Ok(()) +} + +pub fn store_device( + openid_user: super::providers::OpenIdUser, + openid_provider: &str, + access_token: &str, + refresh_token: Option<&str>, +) -> Result<()> { + log::debug!("Storing openid user {:#?}", openid_user); + let conn = rusqlite::Connection::open(PATH)?; + + // TODO + + Ok(()) +} diff --git a/burrow/src/auth/server/mod.rs b/burrow/src/auth/server/mod.rs new file mode 100644 index 0000000..88b3ff3 --- /dev/null +++ b/burrow/src/auth/server/mod.rs @@ -0,0 +1,62 @@ +pub mod db; +pub mod providers; + +use anyhow::Result; +use axum::{http::StatusCode, routing::post, Router}; +use providers::slack::auth; +use tokio::signal; + +pub async fn serve() -> Result<()> { + db::init_db()?; + + let app = Router::new() + .route("/slack-auth", post(auth)) + .route("/device/new", post(device_new)); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); + log::info!("Starting auth server on port 8080"); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); + + Ok(()) +} + +async fn device_new() -> StatusCode { + StatusCode::OK +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} + +// mod db { +// use rusqlite::{Connection, Result}; + +// #[derive(Debug)] +// struct User { +// id: i32, +// created_at: String, +// } +// } diff --git a/burrow/src/auth/server/providers/mod.rs b/burrow/src/auth/server/providers/mod.rs new file mode 100644 index 0000000..36ff0bd --- /dev/null +++ b/burrow/src/auth/server/providers/mod.rs @@ -0,0 +1,8 @@ +pub mod slack; +pub use super::db; + +#[derive(serde::Deserialize, Default, Debug)] +pub struct OpenIdUser { + pub sub: String, + pub name: String, +} diff --git a/burrow/src/auth/server/providers/slack.rs b/burrow/src/auth/server/providers/slack.rs new file mode 100644 index 0000000..581cd1e --- /dev/null +++ b/burrow/src/auth/server/providers/slack.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use axum::{ + extract::Json, + http::StatusCode, + routing::{get, post}, +}; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; + +use super::db::store_connection; + +#[derive(Deserialize)] +pub struct SlackToken { + slack_token: String, +} +pub async fn auth(Json(payload): Json) -> (StatusCode, String) { + let slack_user = match fetch_slack_user(&payload.slack_token).await { + Ok(user) => user, + Err(e) => { + log::error!("Failed to fetch Slack user: {:?}", e); + return (StatusCode::UNAUTHORIZED, String::new()); + } + }; + + log::info!( + "Slack user {} ({}) logged in.", + slack_user.name, + slack_user.sub + ); + + let conn = match store_connection(slack_user, "slack", &payload.slack_token, None) { + Ok(user) => user, + Err(e) => { + log::error!("Failed to fetch Slack user: {:?}", e); + return (StatusCode::UNAUTHORIZED, String::new()); + } + }; + + (StatusCode::OK, String::new()) +} + +async fn fetch_slack_user(access_token: &str) -> Result { + let client = reqwest::Client::new(); + let res = client + .get("https://slack.com/api/openid.connect.userInfo") + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await? + .json::() + .await?; + + let res_ok = res + .get("ok") + .and_then(|v| v.as_bool()) + .ok_or(anyhow::anyhow!("Slack user object not ok!"))?; + + if !res_ok { + return Err(anyhow::anyhow!("Slack user object not ok!")); + } + + Ok(serde_json::from_value(res)?) +} + +// async fn fetch_save_slack_user_data(query: Query) -> anyhow::Result<()> { +// let client = reqwest::Client::new(); +// log::trace!("Code was {}", &query.code); +// let mut url = Url::parse("https://slack.com/api/openid.connect.token")?; + +// { +// let mut q = url.query_pairs_mut(); +// q.append_pair("client_id", &var("CLIENT_ID")?); +// q.append_pair("client_secret", &var("CLIENT_SECRET")?); +// q.append_pair("code", &query.code); +// q.append_pair("grant_type", "authorization_code"); +// q.append_pair("redirect_uri", "https://burrow.rs/callback"); +// } + +// let data = client +// .post(url) +// .send() +// .await? +// .json::() +// .await?; + +// if !data.ok { +// return Err(anyhow::anyhow!("Slack code exchange response not ok!")); +// } + +// if let Some(access_token) = data.access_token { +// log::trace!("Access token is {access_token}"); +// let user = slack::fetch_slack_user(&access_token) +// .await +// .map_err(|err| anyhow::anyhow!("Failed to fetch Slack user info {:#?}", err))?; + +// db::store_user(user, access_token, String::new()) +// .map_err(|_| anyhow::anyhow!("Failed to store user in db"))?; + +// Ok(()) +// } else { +// Err(anyhow::anyhow!("Access token not found in response")) +// } +// } diff --git a/burrow/src/lib.rs b/burrow/src/lib.rs index d9ebf7e..6aae1fb 100644 --- a/burrow/src/lib.rs +++ b/burrow/src/lib.rs @@ -5,6 +5,8 @@ pub mod wireguard; mod daemon; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod database; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +mod auth; pub(crate) mod tracing; #[cfg(target_vendor = "apple")] diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 295373a..ff07d4c 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -7,6 +7,9 @@ pub(crate) mod tracing; #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod wireguard; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +mod auth; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; use tun::TunOptions; @@ -47,12 +50,15 @@ enum Commands { ServerConfig, /// Reload Config ReloadConfig(ReloadConfigArgs), + /// Authentication server + AuthServer, } #[derive(Args)] struct ReloadConfigArgs { #[clap(long, short)] interface_id: String, + } #[derive(Args)] @@ -133,9 +139,10 @@ async fn try_reloadconfig(interface_id: String) -> Result<()> { } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -#[tokio::main(flavor = "current_thread")] +#[tokio::main] async fn main() -> Result<()> { tracing::initialize(); + dotenv::dotenv().ok(); let cli = Cli::parse(); match &cli.command { @@ -145,6 +152,7 @@ async fn main() -> Result<()> { Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, + Commands::AuthServer => crate::auth::server::serve().await?, } Ok(()) @@ -152,5 +160,5 @@ async fn main() -> Result<()> { #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] pub fn main() { - eprintln!("This platform is not supported currently.") + eprintln!("This platform is not supported") } diff --git a/tun/Cargo.toml b/tun/Cargo.toml index 7413f65..1b07833 100644 --- a/tun/Cargo.toml +++ b/tun/Cargo.toml @@ -8,7 +8,7 @@ libc = "0.2" fehler = "1.0" nix = { version = "0.26", features = ["ioctl"] } socket2 = "0.5" -tokio = { version = "1.28", features = [] } +tokio = { version = "1.37", default-features = false, optional = true } byteorder = "1.4" tracing = "0.1" log = "0.4" @@ -19,10 +19,7 @@ futures = { version = "0.3.28", optional = true } [features] serde = ["dep:serde", "dep:schemars"] -tokio = ["tokio/net", "dep:futures"] - -[target.'cfg(feature = "tokio")'.dev-dependencies] -tokio = { features = ["rt", "macros"] } +tokio = ["tokio/net", "dep:tokio", "dep:futures"] [target.'cfg(windows)'.dependencies] lazy_static = "1.4" @@ -37,7 +34,7 @@ windows = { version = "0.48", features = [ [target.'cfg(windows)'.build-dependencies] anyhow = "1.0" bindgen = "0.65" -reqwest = { version = "0.11", features = ["native-tls"] } +reqwest = { version = "0.11" } ssri = { version = "9.0", default-features = false } tokio = { version = "1.28", features = ["rt", "macros"] } zip = { version = "0.6", features = ["deflate"] } From 3c70bc2a5c4a012c0fc31f12f1f5e75fbde67586 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 6 Jul 2024 10:50:14 -0700 Subject: [PATCH 16/25] Remove SwiftLint from Xcode project --- Apple/Burrow.xcodeproj/project.pbxproj | 45 ---------- .../xcshareddata/swiftpm/Package.resolved | 86 ------------------- 2 files changed, 131 deletions(-) delete mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index a3be02d..5c5e80b 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -294,7 +294,6 @@ buildRules = ( ); dependencies = ( - D082527D2B5DEB80005DA378 /* PBXTargetDependency */, ); name = Shared; productName = Shared; @@ -313,7 +312,6 @@ buildRules = ( ); dependencies = ( - D08252792B5DEB78005DA378 /* PBXTargetDependency */, D00117492B30373500D87C25 /* PBXTargetDependency */, ); name = NetworkExtension; @@ -334,7 +332,6 @@ buildRules = ( ); dependencies = ( - D082527B2B5DEB7D005DA378 /* PBXTargetDependency */, D00117472B30373100D87C25 /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); @@ -374,7 +371,6 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( - D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -513,18 +509,6 @@ target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; - D08252792B5DEB78005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D08252782B5DEB78005DA378 /* SwiftLintPlugin */; - }; - D082527B2B5DEB7D005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */; - }; - D082527D2B5DEB80005DA378 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D082527C2B5DEB80005DA378 /* SwiftLintPlugin */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -624,35 +608,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/realm/SwiftLint.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - D08252782B5DEB78005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; - D082527A2B5DEB7D005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; - D082527C2B5DEB80005DA378 /* SwiftLintPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D08252772B5DEB6E005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */; - productName = "plugin:SwiftLintPlugin"; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = D05B9F6A29E39EEC008CB1F9 /* Project object */; } diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 9378372..0000000 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,86 +0,0 @@ -{ - "pins" : [ - { - "identity" : "collectionconcurrencykit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", - "state" : { - "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", - "version" : "0.2.0" - } - }, - { - "identity" : "cryptoswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", - "state" : { - "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c", - "version" : "1.8.1" - } - }, - { - "identity" : "sourcekitten", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/SourceKitten.git", - "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, - { - "identity" : "swiftlint", - "kind" : "remoteSourceControl", - "location" : "https://github.com/realm/SwiftLint.git", - "state" : { - "branch" : "main", - "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f" - } - }, - { - "identity" : "swiftytexttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", - "state" : { - "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version" : "0.9.0" - } - }, - { - "identity" : "swxmlhash", - "kind" : "remoteSourceControl", - "location" : "https://github.com/drmohundro/SWXMLHash.git", - "state" : { - "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", - "version" : "7.0.2" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams.git", - "state" : { - "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", - "version" : "5.0.6" - } - } - ], - "version" : 2 -} From 3dedca4de308a16f1782f3fdc30c6cce5056c8d3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 6 Jul 2024 17:20:46 -0700 Subject: [PATCH 17/25] Update build settings --- .github/actions/notarize/action.yml | 34 +- .github/workflows/build-apple.yml | 4 +- .github/workflows/build-rpm.yml | 2 +- .github/workflows/build-rust.yml | 10 +- .github/workflows/release-apple.yml | 41 +- Apple/App/AppDelegate.swift | 3 +- Apple/App/MainMenu.xib | 4 +- Cargo.lock | 754 ++++++++++++++-------------- Dockerfile | 3 +- 9 files changed, 407 insertions(+), 448 deletions(-) diff --git a/.github/actions/notarize/action.yml b/.github/actions/notarize/action.yml index f3f98f2..efd2159 100644 --- a/.github/actions/notarize/action.yml +++ b/.github/actions/notarize/action.yml @@ -9,12 +9,6 @@ inputs: app-store-key-issuer-id: description: App Store key issuer ID required: true - archive-path: - description: Xcode archive path - required: true - export-path: - description: The path to export the archive to - required: true runs: using: composite steps: @@ -24,28 +18,8 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"export","method":"developer-id"}' \ - | plutil -convert xml1 -o ExportOptions.plist - + ditto -c -k --keepParent Release/Burrow.app Upload.zip + xcrun notarytool submit --wait --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip + xcrun stapler staple Release/Burrow.app - xcodebuild -exportArchive \ - -allowProvisioningUpdates \ - -allowProvisioningDeviceRegistration \ - -skipPackagePluginValidation \ - -skipMacroValidation \ - -onlyUsePackageVersionsFromResolvedFile \ - -authenticationKeyID ${{ inputs.app-store-key-id }} \ - -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ - -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ - -archivePath Wallet.xcarchive \ - -exportPath Release \ - -exportOptionsPlist ExportOptions.plist - - ditto -c -k --keepParent Release/Wallet.app Upload.zip - SUBMISSION_ID=$(xcrun notarytool submit --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | awk '/ id:/ { print $2; exit }') - - xcrun notarytool wait $SUBMISSION_ID --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" - xcrun stapler staple Release/Wallet.app - - aa archive -a lzma -b 8m -d Release -subdir Wallet.app -o Wallet.app.aar - - rm -rf Upload.zip Release AuthKey_${{ inputs.app-store-key-id }}.p8 ExportOptions.plist + rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 Release diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 00b6bec..84cc03a 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -24,7 +24,7 @@ jobs: rust-targets: - aarch64-apple-ios - scheme: App - destination: platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro + destination: platform=iOS Simulator,OS=18.0,name=iPhone 15 Pro platform: iOS Simulator sdk-name: iphonesimulator rust-targets: @@ -38,7 +38,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml index e0ce8df..029bf16 100644 --- a/.github/workflows/build-rpm.yml +++ b/.github/workflows/build-rpm.yml @@ -5,7 +5,7 @@ jobs: name: Build RPM runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Install RPM run: cargo install cargo-generate-rpm diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 3255fc7..76ce9f2 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -22,14 +22,18 @@ jobs: targets: - aarch64-unknown-linux-gnu - os: macos-12 - platform: macOS + platform: macOS (Intel) test-targets: - x86_64-apple-darwin targets: + - x86_64-apple-ios + - os: macos-14 + platform: macOS + test-targets: - aarch64-apple-darwin + targets: - aarch64-apple-ios - aarch64-apple-ios-sim - - x86_64-apple-ios - os: windows-2022 platform: Windows test-targets: @@ -38,7 +42,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 786fb54..1883008 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -13,7 +13,8 @@ jobs: fail-fast: false matrix: include: - - destination: generic/platform=iOS + - + destination: generic/platform=iOS platform: iOS rust-targets: - aarch64-apple-ios @@ -23,7 +24,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v4 @@ -50,25 +51,23 @@ jobs: app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive - - name: Notarize (macOS) - if: ${{ matrix.platform == 'macOS' }} - uses: ./.github/actions/notarize - with: - app-store-key: ${{ secrets.APPSTORE_KEY }} - app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} - app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - archive-path: Burrow.xcarchive - - name: Export IPA (iOS) - if: ${{ matrix.platform == 'iOS' }} + - name: Export uses: ./.github/actions/export with: - method: ad-hoc + method: ${{ matrix.platform == 'macOS' && 'developer-id' || 'ad-hoc' }} destination: export app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive export-path: Release + - name: Notarize + if: ${{ matrix.platform == 'macOS' }} + uses: ./.github/actions/notarize + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - name: Compress (iOS) if: ${{ matrix.platform == 'iOS' }} shell: bash @@ -83,27 +82,17 @@ jobs: aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar rm -rf Apple/Release - - name: Upload to GitHub (iOS) - if: ${{ matrix.platform == 'iOS' }} + - name: Upload to GitHub uses: SierraSoftworks/gh-releases@v1.0.7 with: token: ${{ secrets.GITHUB_TOKEN }} release_tag: ${{ github.ref_name }} overwrite: 'true' files: | - Burrow.ipa - Burrow-${{ matrix.platform }}.xcarchive.aar - - name: Upload to GitHub (macOS) - if: ${{ matrix.platform == 'macOS' }} - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: ${{ github.ref_name }} - overwrite: 'true' - files: | - Burrow.aap.aar + ${{ matrix.platform == 'macOS' && 'Burrow.aap.aar' || 'Burrow.ipa' }} Burrow-${{ matrix.platform }}.xcarchive.aar - name: Upload to App Store Connect + if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: method: app-store diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index 6085d85..bd76a2f 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -2,8 +2,7 @@ import AppKit import SwiftUI -@MainActor -@NSApplicationMain +@MainActor @main class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib index 8933f30..587f6c4 100644 --- a/Apple/App/MainMenu.xib +++ b/Apple/App/MainMenu.xib @@ -1,7 +1,7 @@ - + - + diff --git a/Cargo.lock b/Cargo.lock index ce263f9..5ef886c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" dependencies = [ "cfg-if", "once_cell", @@ -52,57 +52,62 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] -name = "anstream" -version = "0.6.14" +name = "allocator-api2" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -110,17 +115,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-channel" -version = "2.3.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -145,25 +151,25 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "autocfg" -version = "1.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" @@ -176,9 +182,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "itoa", "matchit", "memchr", @@ -236,7 +242,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", "mime", "rustversion", @@ -267,9 +273,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -339,7 +345,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.48", "which", ] @@ -351,9 +357,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blake2" @@ -375,9 +381,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "burrow" @@ -432,9 +438,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" @@ -469,13 +475,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.104" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -530,20 +535,20 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading 0.8.4", + "libloading 0.8.1", ] [[package]] name = "clap" -version = "4.5.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -551,9 +556,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -563,33 +568,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.5.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -677,27 +682,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -712,14 +717,15 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", + "platforms", "rustc_version", "subtle", "zeroize", @@ -733,7 +739,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -764,15 +770,15 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" -version = "1.13.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" @@ -782,9 +788,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -797,9 +803,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", @@ -807,9 +813,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -818,9 +824,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener", "pin-project-lite", @@ -840,9 +846,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fehler" @@ -866,15 +872,15 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -966,7 +972,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1011,9 +1017,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1022,9 +1028,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1034,17 +1040,17 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.26" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.12", - "indexmap 2.2.6", + "http 0.2.11", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1059,20 +1065,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.14.3", ] [[package]] @@ -1090,15 +1097,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.5.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -1126,9 +1133,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1153,7 +1160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.12", + "http 0.2.11", "pin-project-lite", ] @@ -1182,9 +1189,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1200,16 +1207,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", "httparse", "httpdate", @@ -1266,7 +1273,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -1279,7 +1286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", @@ -1327,12 +1334,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.14.3", ] [[package]] @@ -1346,15 +1353,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.39.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ "console", "lazy_static", "linked-hash-map", "serde", "similar", + "yaml-rust", ] [[package]] @@ -1385,50 +1393,44 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" - [[package]] name = "itertools" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" @@ -1438,9 +1440,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -1454,12 +1456,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-sys 0.48.0", ] [[package]] @@ -1499,15 +1501,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1515,9 +1517,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matchers" @@ -1536,9 +1538,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1551,9 +1553,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1578,7 +1580,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1595,18 +1597,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.11" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -1615,10 +1617,11 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ + "lazy_static", "libc", "log", "openssl", @@ -1649,10 +1652,10 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "cfg-if", "libc", - "memoffset 0.9.1", + "memoffset 0.9.0", ] [[package]] @@ -1675,17 +1678,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" -version = "0.2.19" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1702,9 +1699,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1717,17 +1714,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -1744,7 +1741,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -1755,9 +1752,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1779,9 +1776,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -1789,15 +1786,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1837,29 +1834,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1869,9 +1866,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "poly1305" @@ -1898,28 +1901,28 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", "prost-derive", @@ -1927,22 +1930,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "prost-types" -version = "0.12.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -1996,9 +1999,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2035,23 +2038,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -2065,13 +2068,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.2", ] [[package]] @@ -2082,15 +2085,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.7", "bytes", @@ -2098,9 +2101,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -2110,11 +2113,9 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -2151,7 +2152,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -2170,17 +2171,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -2189,7 +2189,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2199,9 +2199,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -2220,11 +2220,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -2245,15 +2245,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2283,15 +2274,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" @@ -2304,9 +2295,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -2316,14 +2307,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.68", + "syn 1.0.109", ] [[package]] @@ -2334,11 +2325,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2347,9 +2338,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -2357,46 +2348,46 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" dependencies = [ "itoa", "ryu", @@ -2484,9 +2475,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.5.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" [[package]] name = "slab" @@ -2499,18 +2490,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -2537,15 +2528,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.6.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -2560,9 +2551,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2604,41 +2595,42 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", + "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if", "once_cell", @@ -2646,12 +2638,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", - "num-conv", "powerfmt", "serde", "time-core", @@ -2665,9 +2656,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" -version = "1.7.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -2715,7 +2706,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -2741,9 +2732,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -2752,15 +2743,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -2775,9 +2767,9 @@ dependencies = [ "base64 0.21.7", "bytes", "h2", - "http 0.2.12", + "http 0.2.11", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -2842,7 +2834,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -2941,7 +2933,7 @@ dependencies = [ "libloading 0.7.4", "log", "nix 0.26.4", - "reqwest 0.11.27", + "reqwest 0.11.23", "schemars", "serde", "socket2", @@ -2974,18 +2966,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "universal-hash" @@ -3005,9 +2997,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3016,15 +3008,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.9.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "serde", ] @@ -3064,9 +3056,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3074,24 +3066,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -3101,9 +3093,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3111,28 +3103,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -3161,9 +3153,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -3211,7 +3203,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.52.0", ] [[package]] @@ -3231,18 +3223,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "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]] @@ -3253,9 +3244,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" @@ -3265,9 +3256,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" @@ -3277,15 +3268,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" @@ -3295,9 +3280,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" @@ -3307,9 +3292,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" @@ -3319,9 +3304,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" @@ -3331,9 +3316,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winreg" @@ -3357,9 +3342,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core", @@ -3369,35 +3354,44 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.11" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -3410,7 +3404,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.48", ] [[package]] @@ -3454,9 +3448,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", "pkg-config", diff --git a/Dockerfile b/Dockerfile index e55eb58..8e17812 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.77-slim-bookworm AS builder +FROM docker.io/library/rust:1.79-slim-bookworm AS builder ARG TARGETPLATFORM ARG LLVM_VERSION=16 @@ -56,7 +56,6 @@ ENV CC_x86_64_unknown_linux_musl=clang-$LLVM_VERSION \ AR_aarch64_unknown_linux_musl=llvm-ar-$LLVM_VERSION \ CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/x86_64-linux-musl -L/lib/x86_64-linux-musl -C linker=rust-lld" \ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-L/usr/lib/aarch64-linux-musl -L/lib/aarch64-linux-musl -C linker=rust-lld" \ - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \ SQLITE3_STATIC=1 \ SQLITE3_INCLUDE_DIR=/usr/local/include \ SQLITE3_LIB_DIR=/usr/local/lib From 951b4ddae2f33e15c7d1976337de64001797d0e9 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 18:09:09 -0700 Subject: [PATCH 18/25] add protobuf definition file --- proto/burrow.proto | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 proto/burrow.proto diff --git a/proto/burrow.proto b/proto/burrow.proto new file mode 100644 index 0000000..3e15219 --- /dev/null +++ b/proto/burrow.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; +package burrow; + +import "google/protobuf/timestamp.proto"; + +service Tunnel { + rpc TunnelConfiguration (Empty) returns (TunnelConfigurationResponse); + rpc TunnelStart (Empty) returns (Empty); + rpc TunnelStop (Empty) returns (Empty); + rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse); +} + +service Networks { + rpc NetworkAdd (Empty) returns (Empty); + rpc NetworkList (Empty) returns (stream NetworkListResponse); + rpc NetworkReorder (NetworkReorderRequest) returns (Empty); + rpc NetworkDelete (NetworkDeleteRequest) returns (Empty); +} + +message NetworkReorderRequest { + int32 id = 1; + int32 index = 2; +} + +message WireGuardPeer { + string endpoint = 1; + repeated string subnet = 2; +} + +message WireGuardNetwork { + string address = 1; + string dns = 2; + repeated WireGuardPeer peer = 3; +} + +message NetworkDeleteRequest { + int32 id = 1; +} + +message Network { + int32 id = 1; + NetworkType type = 2; + bytes payload = 3; +} + +enum NetworkType { + WireGuard = 0; + HackClub = 1; +} + +message NetworkListResponse { + repeated Network network = 1; +} + +message Empty { + +} + +enum State { + Stopped = 0; + Running = 1; +} + +message TunnelStatusResponse { + State state = 1; + optional google.protobuf.Timestamp start = 2; +} + +message TunnelConfigurationResponse { + repeated string addresses = 1; + int32 mtu = 2; +} From aa634d03e2560dbaf500b19f584d19696abb7161 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 18:14:00 -0700 Subject: [PATCH 19/25] update protobuf definition file --- proto/burrow.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/burrow.proto b/proto/burrow.proto index 3e15219..2d29c78 100644 --- a/proto/burrow.proto +++ b/proto/burrow.proto @@ -4,7 +4,7 @@ package burrow; import "google/protobuf/timestamp.proto"; service Tunnel { - rpc TunnelConfiguration (Empty) returns (TunnelConfigurationResponse); + rpc TunnelConfiguration (Empty) returns (stream TunnelConfigurationResponse); rpc TunnelStart (Empty) returns (Empty); rpc TunnelStop (Empty) returns (Empty); rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse); From 62a5739d86feb8c2d23ec3d6187d2f1c6dffc9d3 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:01:17 -0700 Subject: [PATCH 20/25] Update pipelines with various fixes --- .github/actions/download-profiles/action.yml | 27 ++++++++++++ .github/workflows/build-appimage.yml | 3 ++ .github/workflows/build-docker.yml | 3 ++ .github/workflows/build-rust.yml | 2 +- .github/workflows/lint-swift.yml | 2 +- .github/workflows/release-appimage.yml | 29 ------------- .github/workflows/release-apple.yml | 5 ++- .github/workflows/release-if-needed.yaml | 2 + .github/workflows/release-linux.yml | 43 +++++++++----------- 9 files changed, 59 insertions(+), 57 deletions(-) create mode 100644 .github/actions/download-profiles/action.yml delete mode 100644 .github/workflows/release-appimage.yml diff --git a/.github/actions/download-profiles/action.yml b/.github/actions/download-profiles/action.yml new file mode 100644 index 0000000..98961aa --- /dev/null +++ b/.github/actions/download-profiles/action.yml @@ -0,0 +1,27 @@ +name: Download Provisioning Profiles +inputs: + app-store-key: + description: App Store key in PEM PKCS#8 format + required: true + app-store-key-id: + description: App Store key ID + required: true + app-store-key-issuer-id: + description: App Store key issuer ID + required: true +runs: + using: composite + steps: + - shell: bash + run: | + cat << EOF > api-key.json + { + "key_id": "${{ inputs.app-store-key-id }}", + "issuer_id": "${{ inputs.app-store-key-issuer-id }}", + "key": "${{ inputs.app-store-key }}" + } + EOF + + fastlane sigh download_all --api_key_path api-key.json --download_xcode_profiles + + rm -rf api-key.json diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index bb510fb..bd29b07 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -6,6 +6,9 @@ on: pull_request: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: appimage: name: Build AppImage diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 307a93c..6a3dae1 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -6,6 +6,9 @@ on: pull_request: branches: - "*" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: name: Build Docker Image diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 76ce9f2..11ff60d 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -58,7 +58,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y ${{ join(matrix.packages, ' ') }} - - name: Install Windows Deps + - name: Configure LLVM if: matrix.os == 'windows-2022' shell: bash run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml index a2cc96a..857f575 100644 --- a/.github/workflows/lint-swift.yml +++ b/.github/workflows/lint-swift.yml @@ -13,4 +13,4 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Lint - run: swiftlint lint --reporter github-actions-logging + run: swiftlint lint --strict --reporter github-actions-logging diff --git a/.github/workflows/release-appimage.yml b/.github/workflows/release-appimage.yml deleted file mode 100644 index e566186..0000000 --- a/.github/workflows/release-appimage.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Release (AppImage) -on: - release: - types: - - created -jobs: - appimage: - name: Build AppImage - runs-on: ubuntu-latest - container: docker - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Build - run: | - docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile - docker create --name temp appimage-builder - docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . - docker rm temp - - name: Upload to GitHub - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: ${{ github.ref_name }} - overwrite: 'true' - files: | - Burrow-x86_64.AppImage diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index 1883008..f1ee5dd 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -24,7 +24,7 @@ jobs: - x86_64-apple-darwin - aarch64-apple-darwin env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v4 @@ -40,8 +40,9 @@ jobs: with: targets: ${{ join(matrix.rust-targets, ', ') }} - name: Configure Version + id: version shell: bash - run: Tools/version.sh + run: echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT - name: Archive uses: ./.github/actions/archive with: diff --git a/.github/workflows/release-if-needed.yaml b/.github/workflows/release-if-needed.yaml index 0d2eb97..79f0d63 100644 --- a/.github/workflows/release-if-needed.yaml +++ b/.github/workflows/release-if-needed.yaml @@ -9,6 +9,8 @@ jobs: create: name: Create Release If Needed runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 6709edb..7db9bcf 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -2,33 +2,28 @@ name: Release (Linux) on: release: types: - - created + - created jobs: appimage: name: Build AppImage runs-on: ubuntu-latest container: docker steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Build AppImage - run: | - docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile - docker create --name temp appimage-builder - docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . - docker rm temp - - name: Get Build Number - id: version - shell: bash - run: | - echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT - - name: Attach Artifacts - uses: SierraSoftworks/gh-releases@v1.0.7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - release_tag: builds/${{ steps.version.outputs.BUILD_NUMBER }} - overwrite: "true" - files: | - Burrow-x86_64.AppImage + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build AppImage + run: | + docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile + docker create --name temp appimage-builder + docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . + docker rm temp + - name: Attach Artifacts + uses: SierraSoftworks/gh-releases@v1.0.7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release_tag: ${{ github.ref_name }} + overwrite: "true" + files: | + Burrow-x86_64.AppImage From fa1ef6fcda7acf4f9a0cf66d811baf1e626ac2a4 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:08:02 -0700 Subject: [PATCH 21/25] Download provisioning profiles in release pipeline --- .github/actions/download-profiles/action.yml | 7 +++++-- .github/actions/export/action.yml | 10 +++------- .github/workflows/build-rust.yml | 2 +- .github/workflows/release-apple.yml | 21 ++++++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/actions/download-profiles/action.yml b/.github/actions/download-profiles/action.yml index 98961aa..32b615c 100644 --- a/.github/actions/download-profiles/action.yml +++ b/.github/actions/download-profiles/action.yml @@ -13,15 +13,18 @@ runs: using: composite steps: - shell: bash + env: + FASTLANE_OPT_OUT_USAGE: 'YES' run: | + APP_STORE_KEY=$(echo "${{ inputs.app-store-key }}" | jq -sR .) cat << EOF > api-key.json { "key_id": "${{ inputs.app-store-key-id }}", "issuer_id": "${{ inputs.app-store-key-issuer-id }}", - "key": "${{ inputs.app-store-key }}" + "key": $APP_STORE_KEY } EOF - fastlane sigh download_all --api_key_path api-key.json --download_xcode_profiles + fastlane sigh download_all --api_key_path api-key.json rm -rf api-key.json diff --git a/.github/actions/export/action.yml b/.github/actions/export/action.yml index 8f891be..75b748f 100644 --- a/.github/actions/export/action.yml +++ b/.github/actions/export/action.yml @@ -12,11 +12,8 @@ inputs: archive-path: description: Xcode archive path required: true - destination: - description: The Xcode export destination. This can either be "export" or "upload" - required: true - method: - description: The Xcode export method. This can be one of app-store, validation, ad-hoc, package, enterprise, development, developer-id, or mac-application. + export-options: + description: The export options in JSON format required: true export-path: description: The path to export the archive to @@ -29,8 +26,7 @@ runs: run: | echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 - echo '{"destination":"${{ inputs.destination }}","method":"${{ inputs.method }}"}' \ - | plutil -convert xml1 -o ExportOptions.plist - + echo '${{ inputs.export-options }}' | plutil -convert xml1 -o ExportOptions.plist - xcodebuild \ -exportArchive \ diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 11ff60d..22bf83a 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -42,7 +42,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index f1ee5dd..bb9c15a 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -13,13 +13,10 @@ jobs: fail-fast: false matrix: include: - - - destination: generic/platform=iOS - platform: iOS + - platform: iOS rust-targets: - aarch64-apple-ios - - destination: generic/platform=macOS - platform: macOS + - platform: macOS rust-targets: - x86_64-apple-darwin - aarch64-apple-darwin @@ -35,6 +32,12 @@ jobs: with: certificate: ${{ secrets.DEVELOPER_CERT }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} + - name: Download Provisioning Profiles + uses: ./.github/actions/download-profiles + with: + app-store-key: ${{ secrets.APPSTORE_KEY }} + app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} + app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -47,7 +50,7 @@ jobs: uses: ./.github/actions/archive with: scheme: App - destination: ${{ matrix.destination }} + destination: generic/platform=${{ matrix.platform }} app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} @@ -61,6 +64,8 @@ jobs: app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive + export-options: | + {"teamID":"P6PV2R9443","destination":"export","method":"developer-id","provisioningProfiles":{"com.hackclub.burrow":"Burrow Developer ID","com.hackclub.burrow.network":"Burrow Network Developer ID"},"signingCertificate":"Developer ID Application","signingStyle":"manual"} export-path: Release - name: Notarize if: ${{ matrix.platform == 'macOS' }} @@ -96,10 +101,10 @@ jobs: if: ${{ matrix.platform == 'iOS' }} uses: ./.github/actions/export with: - method: app-store - destination: upload app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} archive-path: Burrow.xcarchive + export-options: | + {"method": "app-store", "destination": "upload"} export-path: Release From 3fbb520a106101761ca3cff49ce62029a88408fa Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 17:36:48 -0700 Subject: [PATCH 22/25] Fix SwiftLint errors --- .github/actions/build-for-testing/action.yml | 2 + .github/workflows/build-rust.yml | 6 ++- Apple/App/AppDelegate.swift | 3 +- Apple/App/BurrowView.swift | 1 - Apple/App/OAuth2.swift | 46 +++++++++++--------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.github/actions/build-for-testing/action.yml b/.github/actions/build-for-testing/action.yml index 084ba81..185c4ab 100644 --- a/.github/actions/build-for-testing/action.yml +++ b/.github/actions/build-for-testing/action.yml @@ -27,7 +27,9 @@ runs: Apple/DerivedData key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} restore-keys: | + ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} ${{ runner.os }}-${{ inputs.scheme }}- + ${{ runner.os }}- - name: Build shell: bash working-directory: Apple diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 22bf83a..84ac9d8 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -21,14 +21,16 @@ jobs: - x86_64-unknown-linux-gnu targets: - aarch64-unknown-linux-gnu - - os: macos-12 + - os: macos-13 platform: macOS (Intel) + xcode: /Applications/Xcode_15.2.app test-targets: - x86_64-apple-darwin targets: - x86_64-apple-ios - os: macos-14 platform: macOS + xcode: /Applications/Xcode_16.0.app test-targets: - aarch64-apple-darwin targets: @@ -42,7 +44,7 @@ jobs: - aarch64-pc-windows-msvc runs-on: ${{ matrix.os }} env: - DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + DEVELOPER_DIR: ${{ matrix.xcode }}/Contents/Developer CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index bd76a2f..b0c5546 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -2,7 +2,8 @@ import AppKit import SwiftUI -@MainActor @main +@main +@MainActor class AppDelegate: NSObject, NSApplicationDelegate { private let quitItem: NSMenuItem = { let quitItem = NSMenuItem( diff --git a/Apple/App/BurrowView.swift b/Apple/App/BurrowView.swift index 8447592..3a53762 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/App/BurrowView.swift @@ -42,7 +42,6 @@ struct BurrowView: View { } private func addWireGuardNetwork() { - } private func authenticateWithSlack() async throws { diff --git a/Apple/App/OAuth2.swift b/Apple/App/OAuth2.swift index dc8c62b..9a930c9 100644 --- a/Apple/App/OAuth2.swift +++ b/Apple/App/OAuth2.swift @@ -1,6 +1,6 @@ import AuthenticationServices -import SwiftUI import Foundation +import SwiftUI enum OAuth2 { enum Error: Swift.Error { @@ -35,7 +35,7 @@ enum OAuth2 { } } - public init( + init( authorizationEndpoint: URL, tokenEndpoint: URL, redirectURI: URL, @@ -125,7 +125,11 @@ enum OAuth2 { var refreshToken: String? var credential: Credential { - .init(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expiresIn.map { Date.init(timeIntervalSinceNow: $0) }) + .init( + accessToken: accessToken, + refreshToken: refreshToken, + expirationDate: expiresIn.map { Date(timeIntervalSinceNow: $0) } + ) } } @@ -203,7 +207,24 @@ enum OAuth2 { } extension WebAuthenticationSession { - func start(url: URL, redirectURI: URL) async throws -> URL { +#if canImport(BrowserEngineKit) + @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) + fileprivate static func callback(for redirectURI: URL) throws -> ASWebAuthenticationSession.Callback { + switch redirectURI.scheme { + case "https": + guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } + return .https(host: host, path: redirectURI.path) + case "http": + throw OAuth2.Error.invalidRedirectURI + case .some(let scheme): + return .customScheme(scheme) + case .none: + throw OAuth2.Error.invalidRedirectURI + } + } +#endif + + fileprivate func start(url: URL, redirectURI: URL) async throws -> URL { #if canImport(BrowserEngineKit) if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { return try await authenticate( @@ -231,23 +252,6 @@ extension WebAuthenticationSession { return url } } - - #if canImport(BrowserEngineKit) - @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) - fileprivate static func callback(for redirectURI: URL) throws -> ASWebAuthenticationSession.Callback { - switch redirectURI.scheme { - case "https": - guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI } - return .https(host: host, path: redirectURI.path) - case "http": - throw OAuth2.Error.invalidRedirectURI - case .some(let scheme): - return .customScheme(scheme) - case .none: - throw OAuth2.Error.invalidRedirectURI - } - } - #endif } extension View { From e4b0f1660bff2112d0e20316c228926bed47f270 Mon Sep 17 00:00:00 2001 From: Jett Chen Date: Sat, 13 Jul 2024 17:32:49 -0700 Subject: [PATCH 23/25] GRPC Server Support - Deprecates old json-rpc system - Add GRPC daemon over uds --- .github/workflows/build-apple.yml | 9 +- .github/workflows/build-rust.yml | 7 +- .gitignore | 5 + .vscode/settings.json | 9 +- .../NetworkExtension/libburrow/build-rust.sh | 2 + Cargo.lock | 341 +++++++++++++++++- Dockerfile | 2 +- Makefile | 6 + burrow/Cargo.toml | 18 +- burrow/build.rs | 4 + burrow/burrow.db | Bin 20480 -> 0 bytes burrow/src/auth/server/db.rs | 2 + burrow/src/daemon/instance.rs | 300 ++++++++++----- burrow/src/daemon/mod.rs | 72 ++-- burrow/src/daemon/net/mod.rs | 9 +- burrow/src/daemon/net/unix.rs | 4 +- burrow/src/daemon/rpc/client.rs | 31 ++ burrow/src/daemon/rpc/grpc_defs.rs | 5 + burrow/src/daemon/rpc/mod.rs | 3 + burrow/src/database.rs | 109 +++++- burrow/src/main.rs | 156 +++++++- burrow/src/wireguard/config.rs | 94 ++++- burrow/src/wireguard/iface.rs | 14 +- burrow/src/wireguard/inifield.rs | 81 +++++ burrow/src/wireguard/mod.rs | 1 + ...guard__config__tests__tst_config_toml.snap | 16 + burrow/tmp/conrd.conf | 8 + proto/burrow.proto | 2 +- 28 files changed, 1110 insertions(+), 200 deletions(-) create mode 100644 burrow/build.rs delete mode 100644 burrow/burrow.db create mode 100644 burrow/src/daemon/rpc/client.rs create mode 100644 burrow/src/daemon/rpc/grpc_defs.rs create mode 100644 burrow/src/wireguard/inifield.rs create mode 100644 burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap create mode 100644 burrow/tmp/conrd.conf diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 84cc03a..b628001 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -1,7 +1,7 @@ name: Build Apple Apps on: push: - branches: + branches: - main pull_request: branches: @@ -39,6 +39,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + PROTOC_VERSION: 3.25.1 steps: - name: Checkout uses: actions/checkout@v3 @@ -54,6 +55,10 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Install protoc + uses: taiki-e/install-action@v2 + with: + tool: protoc@${{ env.PROTOC_VERSION }} - name: Build id: build uses: ./.github/actions/build-for-testing @@ -82,4 +87,4 @@ jobs: destination: ${{ matrix.destination }} test-plan: ${{ matrix.xcode-ui-test }} artifact-prefix: ui-tests-${{ matrix.sdk-name }} - check-name: Xcode UI Tests (${{ matrix.platform }}) + check-name: Xcode UI Tests (${{ matrix.platform }}) \ No newline at end of file diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml index 84ac9d8..95fc628 100644 --- a/.github/workflows/build-rust.yml +++ b/.github/workflows/build-rust.yml @@ -48,6 +48,7 @@ jobs: CARGO_INCREMENTAL: 0 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc RUST_BACKTRACE: short + PROTOC_VERSION: 3.25.1 steps: - name: Checkout uses: actions/checkout@v3 @@ -64,6 +65,10 @@ jobs: if: matrix.os == 'windows-2022' shell: bash run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH + - name: Install protoc + uses: taiki-e/install-action@v2 + with: + tool: protoc@${{ env.PROTOC_VERSION }} - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -77,4 +82,4 @@ jobs: run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} - name: Test shell: bash - run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} + run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96b2507..997d4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ target/ .DS_STORE .idea/ + +tmp/ + +*.db +*.sock \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a760137..eb85504 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,12 @@ "rust-analyzer.inlayHints.typeHints.enable": false, "rust-analyzer.linkedProjects": [ "./burrow/Cargo.toml" - ] + ], + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "diffEditor.ignoreTrimWhitespace": false, + "editor.formatOnSave": false + } } diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index e7204a5..00c3652 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -68,6 +68,8 @@ else CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi +CARGO_PATH="$(dirname $(readlink -f $(which protoc))):$CARGO_PATH" + # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" diff --git a/Cargo.lock b/Cargo.lock index 5ef886c..309fc08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,17 +132,38 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" +dependencies = [ + "async-stream-impl 0.2.1", + "futures-core", +] + [[package]] name = "async-stream" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ - "async-stream-impl", + "async-stream-impl 0.3.5", "futures-core", "pin-project-lite", ] +[[package]] +name = "async-stream-impl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-stream-impl" version = "0.3.5" @@ -165,6 +186,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.1.0" @@ -392,6 +419,8 @@ dependencies = [ "aead", "anyhow", "async-channel", + "async-stream 0.2.1", + "async-stream 0.2.1", "axum 0.7.5", "base64 0.21.7", "blake2", @@ -404,6 +433,7 @@ dependencies = [ "fehler", "futures", "hmac", + "hyper-util", "insta", "ip_network", "ip_network_table", @@ -412,15 +442,24 @@ dependencies = [ "nix 0.27.1", "once_cell", "parking_lot", + "prost 0.13.1", + "prost-types 0.13.1", + "prost 0.13.2", + "prost-types 0.13.2", "rand", "rand_core", "reqwest 0.12.5", "ring", "rusqlite", + "rust-ini", "schemars", "serde", "serde_json", "tokio", + "tokio-stream", + "tonic 0.12.2", + "tonic-build", + "tower", "tracing", "tracing-journald", "tracing-log 0.1.4", @@ -619,9 +658,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", - "prost", - "prost-types", - "tonic", + "prost 0.12.3", + "prost-types 0.12.3", + "tonic 0.10.2", "tracing-core", ] @@ -637,18 +676,38 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types", + "prost-types 0.12.3", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic", + "tonic 0.10.2", "tracing", "tracing-core", "tracing-subscriber", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -704,6 +763,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -762,6 +827,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -876,6 +950,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -1057,6 +1137,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1215,7 +1314,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "httparse", @@ -1238,6 +1337,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1279,6 +1379,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1615,6 +1728,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "native-tls" version = "0.2.11" @@ -1762,6 +1881,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.3", +] + [[package]] name = "overload" version = "0.1.1" @@ -1832,6 +1961,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.1.0", +] + [[package]] name = "pin-project" version = "1.1.4" @@ -1925,7 +2064,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.3", +] + +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive 0.13.2", +] + +[[package]] +name = "prost-build" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" +dependencies = [ + "bytes", + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.13.2", + "prost-types 0.13.2", + "regex", + "syn 2.0.48", + "tempfile", ] [[package]] @@ -1941,13 +2111,35 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "prost-types" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost", + "prost 0.12.3", +] + +[[package]] +name = "prost-types" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +dependencies = [ + "prost 0.13.2", ] [[package]] @@ -2100,7 +2292,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -2197,6 +2389,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2404,6 +2607,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2654,6 +2866,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2755,25 +2976,59 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ - "async-stream", + "async-stream 0.3.5", "async-trait", "axum 0.6.20", "base64 0.21.7", "bytes", - "h2", + "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", - "prost", + "prost 0.12.3", "tokio", "tokio-stream", "tower", @@ -2782,6 +3037,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" +dependencies = [ + "async-stream 0.3.5", + "async-trait", + "axum 0.7.5", + "base64 0.22.1", + "bytes", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-timeout 0.5.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.2", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4ee8877250136bd7e3d2331632810a4df4ea5e004656990d8d66d2f5ee8a67" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 2.0.48", +] + [[package]] name = "tower" version = "0.4.13" @@ -2913,6 +3211,12 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -3320,6 +3624,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Dockerfile b/Dockerfile index 8e17812..404179b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN set -eux && \ curl --proto '=https' --tlsv1.2 -sSf https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor --output $KEYRINGS/llvm.gpg && \ echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-$LLVM_VERSION main" > /etc/apt/sources.list.d/llvm.list && \ apt-get update && \ - apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev && \ + apt-get install --no-install-recommends -y clang-$LLVM_VERSION llvm-$LLVM_VERSION lld-$LLVM_VERSION build-essential sqlite3 libsqlite3-dev musl musl-tools musl-dev protobuf-compiler libprotobuf-dev && \ ln -s clang-$LLVM_VERSION /usr/bin/clang && \ ln -s clang /usr/bin/clang++ && \ ln -s lld-$LLVM_VERSION /usr/bin/ld.lld && \ diff --git a/Makefile b/Makefile index d0c9bd9..6563ab1 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,12 @@ start: stop: @$(cargo_norm) stop +status: + @$(cargo_norm) server-status + +tunnel-config: + @$(cargo_norm) tunnel-config + test-dns: @sudo route delete 8.8.8.8 @sudo route add 8.8.8.8 -interface $(tun) diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index 0fb63a5..d5e56c1 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -19,6 +19,7 @@ tokio = { version = "1.37", features = [ "signal", "time", "tracing", + "fs", ] } tun = { version = "0.1", path = "../tun", features = ["serde", "tokio"] } clap = { version = "4.4", features = ["derive"] } @@ -56,8 +57,17 @@ reqwest = { version = "0.12", default-features = false, features = [ "json", "rustls-tls", ] } -rusqlite = "0.31.0" +rusqlite = { version = "0.31.0", features = ["blob"] } dotenv = "0.15.0" +tonic = "0.12.0" +prost = "0.13.1" +prost-types = "0.13.1" +tokio-stream = "0.1" +async-stream = "0.2" +tower = "0.4.13" +hyper-util = "0.1.6" +toml = "0.8.15" +rust-ini = "0.21.0" [target.'cfg(target_os = "linux")'.dependencies] caps = "0.5" @@ -66,7 +76,7 @@ tracing-journald = "0.3" [target.'cfg(target_vendor = "apple")'.dependencies] nix = { version = "0.27" } -rusqlite = { version = "0.31.0", features = ["bundled"] } +rusqlite = { version = "0.31.0", features = ["bundled", "blob"] } [dev-dependencies] insta = { version = "1.32", features = ["yaml"] } @@ -83,3 +93,7 @@ pre_uninstall_script = "../package/rpm/pre_uninstall" [features] tokio-console = ["dep:console-subscriber"] bundled = ["rusqlite/bundled"] + + +[build-dependencies] +tonic-build = "0.12.0" diff --git a/burrow/build.rs b/burrow/build.rs new file mode 100644 index 0000000..8eea5dc --- /dev/null +++ b/burrow/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_build::compile_protos("../proto/burrow.proto")?; + Ok(()) +} diff --git a/burrow/burrow.db b/burrow/burrow.db deleted file mode 100644 index c5b6e2c614ecb4db4c264f50691b6f2cea99772a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU=U|uU|?lH0A>aT1{MUDff0#~iz&{a zSJuG`(#R{q!1sc08t)Wd5nPH##YaP6Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONfZicc z$HFcyEzQ^%T#}fSlbV-WQl4Lw4W(F}gIpa$TopnboqSvspn?h-TnbQ-nOBlpl$MyB z8lRb>;OQ5l5ajCS8szHd>>8|4o*oaE*2qlJRPgsx2n}!n8RzU6?Cj{`3N}Wwv7Q<1 zfaXxJ1Ip9m3sO^ypcD&=1E7LbbAS%m1t7nq=A{(mXXceCgt$h8DERq@DENi?_#os9 zN|SOjljE~fD{-kv%*n|wPfdx>EGWjMq@XCZI3uwrH3e=C*nZ6bCN^HWN82kl9QqrXkB9 z2QfHiUEN)S6as=geI0`$6}(*|6&yoD{5}1ggIs-G{X!4{1#$w|{|KR+%;J*Ny!e9r zq7qOV0hxr5%q=O!6f7vpEK4j&g$EOs2uVyyDM~HI8Pq9xXi|`n2KCLkcwHFyck!3- z>+!wdTf`T`C&qh$w~N<>-uZ6SzR?gE4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R80;b7 z!o|VBz|4@U&dYEr$KN#|EG0iT)hDDP#M7y)%s3>n*efVK)xe{`($6khIH_U^2USdAr-~_TR567W$rN|LLQhA3=b(zL9R1|XAY71JH1mFA{7sYRJyiIoQ0`5rzQLB0iH+K#3bj)4Xl&R*Ju=HcZQ zhK~MaAtqSUX|Z`ug??_jc2R1Wt8borUSVowq<>&`m5YU0o{@HXWL|}#uVrO=rh!Ga zZ6hlS#2qXH?G9#$JD3OB9ZV2+Fb%LfSQyzjLFL%MIs?@IXAq!ac|B_MXb6mkz-S1J ihQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2B~hX4R^(?&G_ diff --git a/burrow/src/auth/server/db.rs b/burrow/src/auth/server/db.rs index b74f7ce..995e64b 100644 --- a/burrow/src/auth/server/db.rs +++ b/burrow/src/auth/server/db.rs @@ -1,5 +1,7 @@ use anyhow::Result; +use crate::daemon::rpc::grpc_defs::{Network, NetworkType}; + pub static PATH: &str = "./server.sqlite3"; pub fn init_db() -> Result<()> { diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index bc506bd..ce96fa5 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -1,13 +1,30 @@ use std::{ + ops::Deref, path::{Path, PathBuf}, sync::Arc, + time::Duration, }; use anyhow::Result; -use tokio::{sync::RwLock, task::JoinHandle}; +use rusqlite::Connection; +use tokio::sync::{mpsc, watch, Notify, RwLock}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{Request, Response, Status as RspStatus}; use tracing::{debug, info, warn}; -use tun::tokio::TunInterface; +use tun::{tokio::TunInterface, TunOptions}; +use super::rpc::grpc_defs::{ + networks_server::Networks, + tunnel_server::Tunnel, + Empty, + Network, + NetworkDeleteRequest, + NetworkListResponse, + NetworkReorderRequest, + State as RPCTunnelState, + TunnelConfigurationResponse, + TunnelStatusResponse, +}; use crate::{ daemon::rpc::{ DaemonCommand, @@ -17,114 +34,223 @@ use crate::{ ServerConfig, ServerInfo, }, - database::{get_connection, load_interface}, + database::{ + add_network, + delete_network, + get_connection, + list_networks, + load_interface, + reorder_network, + }, wireguard::{Config, Interface}, }; +#[derive(Debug, Clone)] enum RunState { - Running(JoinHandle>), + Running, Idle, } -pub struct DaemonInstance { - rx: async_channel::Receiver, - sx: async_channel::Sender, - subx: async_channel::Sender, +impl RunState { + pub fn to_rpc(&self) -> RPCTunnelState { + match self { + RunState::Running => RPCTunnelState::Running, + RunState::Idle => RPCTunnelState::Stopped, + } + } +} + +#[derive(Clone)] +pub struct DaemonRPCServer { tun_interface: Arc>>, wg_interface: Arc>, config: Arc>, db_path: Option, - wg_state: RunState, + wg_state_chan: (watch::Sender, watch::Receiver), + network_update_chan: (watch::Sender<()>, watch::Receiver<()>), } -impl DaemonInstance { +impl DaemonRPCServer { pub fn new( - rx: async_channel::Receiver, - sx: async_channel::Sender, - subx: async_channel::Sender, wg_interface: Arc>, config: Arc>, db_path: Option<&Path>, - ) -> Self { - Self { - rx, - sx, - subx, - wg_interface, + ) -> Result { + Ok(Self { tun_interface: Arc::new(RwLock::new(None)), + wg_interface, config, db_path: db_path.map(|p| p.to_owned()), - wg_state: RunState::Idle, - } + wg_state_chan: watch::channel(RunState::Idle), + network_update_chan: watch::channel(()), + }) } - async fn proc_command(&mut self, command: DaemonCommand) -> Result { - info!("Daemon got command: {:?}", command); - match command { - DaemonCommand::Start(st) => { - match self.wg_state { - RunState::Running(_) => { - warn!("Got start, but tun interface already up."); - } - RunState::Idle => { - let tun_if = st.tun.open()?; - debug!("Setting tun on wg_interface"); - self.wg_interface.read().await.set_tun(tun_if).await; - debug!("tun set on wg_interface"); - - debug!("Setting tun_interface"); - self.tun_interface = self.wg_interface.read().await.get_tun(); - debug!("tun_interface set: {:?}", self.tun_interface); - - debug!("Cloning wg_interface"); - let tmp_wg = self.wg_interface.clone(); - let run_task = tokio::spawn(async move { - let twlock = tmp_wg.read().await; - twlock.run().await - }); - self.wg_state = RunState::Running(run_task); - info!("Daemon started tun interface"); - } - } - Ok(DaemonResponseData::None) - } - DaemonCommand::ServerInfo => match &self.tun_interface.read().await.as_ref() { - None => Ok(DaemonResponseData::None), - Some(ti) => { - info!("{:?}", ti); - Ok(DaemonResponseData::ServerInfo(ServerInfo::try_from( - ti.inner.get_ref(), - )?)) - } - }, - DaemonCommand::Stop => { - self.wg_interface.read().await.remove_tun().await; - self.wg_state = RunState::Idle; - Ok(DaemonResponseData::None) - } - DaemonCommand::ServerConfig => { - Ok(DaemonResponseData::ServerConfig(ServerConfig::default())) - } - DaemonCommand::ReloadConfig(interface_id) => { - let conn = get_connection(self.db_path.as_deref())?; - let cfig = load_interface(&conn, &interface_id)?; - *self.config.write().await = cfig; - self.subx - .send(DaemonNotification::ConfigChange(ServerConfig::try_from( - &self.config.read().await.to_owned(), - )?)) - .await?; - Ok(DaemonResponseData::None) - } - } + pub fn get_connection(&self) -> Result { + get_connection(self.db_path.as_deref()).map_err(proc_err) } - pub async fn run(&mut self) -> Result<()> { - while let Ok(command) = self.rx.recv().await { - let response = self.proc_command(command).await; - info!("Daemon response: {:?}", response); - self.sx.send(DaemonResponse::new(response)).await?; - } - Ok(()) + async fn set_wg_state(&self, state: RunState) -> Result<(), RspStatus> { + self.wg_state_chan.0.send(state).map_err(proc_err) + } + + async fn get_wg_state(&self) -> RunState { + self.wg_state_chan.1.borrow().to_owned() + } + + async fn notify_network_update(&self) -> Result<(), RspStatus> { + self.network_update_chan.0.send(()).map_err(proc_err) + } +} + +#[tonic::async_trait] +impl Tunnel for DaemonRPCServer { + type TunnelConfigurationStream = ReceiverStream>; + type TunnelStatusStream = ReceiverStream>; + + async fn tunnel_configuration( + &self, + _request: Request, + ) -> Result, RspStatus> { + let (tx, rx) = mpsc::channel(10); + tokio::spawn(async move { + let serv_config = ServerConfig::default(); + tx.send(Ok(TunnelConfigurationResponse { + mtu: serv_config.mtu.unwrap_or(1000), + addresses: serv_config.address, + })) + .await + }); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn tunnel_start(&self, _request: Request) -> Result, RspStatus> { + let wg_state = self.get_wg_state().await; + match wg_state { + RunState::Idle => { + let tun_if = TunOptions::new().open()?; + debug!("Setting tun on wg_interface"); + self.tun_interface.write().await.replace(tun_if); + self.wg_interface + .write() + .await + .set_tun_ref(self.tun_interface.clone()) + .await; + debug!("tun set on wg_interface"); + + debug!("Setting tun_interface"); + debug!("tun_interface set: {:?}", self.tun_interface); + + debug!("Cloning wg_interface"); + let tmp_wg = self.wg_interface.clone(); + let run_task = tokio::spawn(async move { + let twlock = tmp_wg.read().await; + twlock.run().await + }); + self.set_wg_state(RunState::Running).await?; + } + + RunState::Running => { + warn!("Got start, but tun interface already up."); + } + } + + return Ok(Response::new(Empty {})); + } + + async fn tunnel_stop(&self, _request: Request) -> Result, RspStatus> { + self.wg_interface.write().await.remove_tun().await; + self.set_wg_state(RunState::Idle).await?; + return Ok(Response::new(Empty {})); + } + + async fn tunnel_status( + &self, + _request: Request, + ) -> Result, RspStatus> { + let (tx, rx) = mpsc::channel(10); + let mut state_rx = self.wg_state_chan.1.clone(); + tokio::spawn(async move { + let cur = state_rx.borrow_and_update().to_owned(); + tx.send(Ok(status_rsp(cur))).await; + loop { + state_rx.changed().await.unwrap(); + let cur = state_rx.borrow().to_owned(); + let res = tx.send(Ok(status_rsp(cur))).await; + if res.is_err() { + eprintln!("Tunnel status channel closed"); + break; + } + } + }); + Ok(Response::new(ReceiverStream::new(rx))) + } +} + +#[tonic::async_trait] +impl Networks for DaemonRPCServer { + type NetworkListStream = ReceiverStream>; + + async fn network_add(&self, request: Request) -> Result, RspStatus> { + let conn = self.get_connection()?; + let network = request.into_inner(); + add_network(&conn, &network).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } + + async fn network_list( + &self, + _request: Request, + ) -> Result, RspStatus> { + debug!("Mock network_list called"); + let (tx, rx) = mpsc::channel(10); + let conn = self.get_connection()?; + let mut sub = self.network_update_chan.1.clone(); + tokio::spawn(async move { + loop { + let networks = list_networks(&conn) + .map(|res| NetworkListResponse { network: res }) + .map_err(proc_err); + let res = tx.send(networks).await; + if res.is_err() { + eprintln!("Network list channel closed"); + break; + } + sub.changed().await.unwrap(); + } + }); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn network_reorder( + &self, + request: Request, + ) -> Result, RspStatus> { + let conn = self.get_connection()?; + reorder_network(&conn, request.into_inner()).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } + + async fn network_delete( + &self, + request: Request, + ) -> Result, RspStatus> { + let conn = self.get_connection()?; + delete_network(&conn, request.into_inner()).map_err(proc_err)?; + self.notify_network_update().await?; + Ok(Response::new(Empty {})) + } +} + +fn proc_err(err: impl ToString) -> RspStatus { + RspStatus::internal(err.to_string()) +} + +fn status_rsp(state: RunState) -> TunnelStatusResponse { + TunnelStatusResponse { + state: state.to_rpc().into(), + start: None, // TODO: Add timestamp } } diff --git a/burrow/src/daemon/mod.rs b/burrow/src/daemon/mod.rs index 4469e90..f6b973f 100644 --- a/burrow/src/daemon/mod.rs +++ b/burrow/src/daemon/mod.rs @@ -5,14 +5,20 @@ mod instance; mod net; pub mod rpc; -use anyhow::Result; -use instance::DaemonInstance; -pub use net::{DaemonClient, Listener}; +use anyhow::{Error as AhError, Result}; +use instance::DaemonRPCServer; +pub use net::{get_socket_path, DaemonClient}; pub use rpc::{DaemonCommand, DaemonResponseData, DaemonStartOptions}; -use tokio::sync::{Notify, RwLock}; +use tokio::{ + net::UnixListener, + sync::{Notify, RwLock}, +}; +use tokio_stream::wrappers::UnixListenerStream; +use tonic::transport::Server; use tracing::{error, info}; use crate::{ + daemon::rpc::grpc_defs::{networks_server::NetworksServer, tunnel_server::TunnelServer}, database::{get_connection, load_interface}, wireguard::Interface, }; @@ -22,52 +28,36 @@ pub async fn daemon_main( db_path: Option<&Path>, notify_ready: Option>, ) -> Result<()> { - let (commands_tx, commands_rx) = async_channel::unbounded(); - let (response_tx, response_rx) = async_channel::unbounded(); - let (subscribe_tx, subscribe_rx) = async_channel::unbounded(); - - let listener = if let Some(path) = socket_path { - info!("Creating listener... {:?}", path); - Listener::new_with_path(commands_tx, response_rx, subscribe_rx, path) - } else { - info!("Creating listener..."); - Listener::new(commands_tx, response_rx, subscribe_rx) - }; if let Some(n) = notify_ready { n.notify_one() } - let listener = listener?; let conn = get_connection(db_path)?; let config = load_interface(&conn, "1")?; - let iface: Interface = config.clone().try_into()?; - let mut instance = DaemonInstance::new( - commands_rx, - response_tx, - subscribe_tx, - Arc::new(RwLock::new(iface)), + let burrow_server = DaemonRPCServer::new( + Arc::new(RwLock::new(config.clone().try_into()?)), Arc::new(RwLock::new(config)), - db_path, - ); + db_path.clone(), + )?; + let spp = socket_path.clone(); + let tmp = get_socket_path(); + let sock_path = spp.unwrap_or(Path::new(tmp.as_str())); + if sock_path.exists() { + std::fs::remove_file(sock_path)?; + } + let uds = UnixListener::bind(sock_path)?; + let serve_job = tokio::spawn(async move { + let uds_stream = UnixListenerStream::new(uds); + let _srv = Server::builder() + .add_service(TunnelServer::new(burrow_server.clone())) + .add_service(NetworksServer::new(burrow_server)) + .serve_with_incoming(uds_stream) + .await?; + Ok::<(), AhError>(()) + }); info!("Starting daemon..."); - let main_job = tokio::spawn(async move { - let result = instance.run().await; - if let Err(e) = result.as_ref() { - error!("Instance exited: {}", e); - } - result - }); - - let listener_job = tokio::spawn(async move { - let result = listener.run().await; - if let Err(e) = result.as_ref() { - error!("Listener exited: {}", e); - } - result - }); - - tokio::try_join!(main_job, listener_job) + tokio::try_join!(serve_job) .map(|_| ()) .map_err(|e| e.into()) } diff --git a/burrow/src/daemon/net/mod.rs b/burrow/src/daemon/net/mod.rs index 242f479..eb45335 100644 --- a/burrow/src/daemon/net/mod.rs +++ b/burrow/src/daemon/net/mod.rs @@ -1,18 +1,11 @@ - - - - - #[cfg(target_family = "unix")] mod unix; #[cfg(target_family = "unix")] -pub use unix::{DaemonClient, Listener}; +pub use unix::{get_socket_path, DaemonClient, Listener}; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::{DaemonClient, Listener}; - - diff --git a/burrow/src/daemon/net/unix.rs b/burrow/src/daemon/net/unix.rs index 70c4207..975c470 100644 --- a/burrow/src/daemon/net/unix.rs +++ b/burrow/src/daemon/net/unix.rs @@ -25,7 +25,7 @@ const UNIX_SOCKET_PATH: &str = "/run/burrow.sock"; #[cfg(target_vendor = "apple")] const UNIX_SOCKET_PATH: &str = "burrow.sock"; -fn get_socket_path() -> String { +pub fn get_socket_path() -> String { if std::env::var("BURROW_SOCKET_PATH").is_ok() { return std::env::var("BURROW_SOCKET_PATH").unwrap(); } @@ -36,7 +36,7 @@ pub struct Listener { cmd_tx: async_channel::Sender, rsp_rx: async_channel::Receiver, sub_chan: async_channel::Receiver, - inner: UnixListener, + pub inner: UnixListener, } impl Listener { diff --git a/burrow/src/daemon/rpc/client.rs b/burrow/src/daemon/rpc/client.rs new file mode 100644 index 0000000..862e34c --- /dev/null +++ b/burrow/src/daemon/rpc/client.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use hyper_util::rt::TokioIo; +use tokio::net::UnixStream; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; + +use super::grpc_defs::{networks_client::NetworksClient, tunnel_client::TunnelClient}; +use crate::daemon::get_socket_path; + +pub struct BurrowClient { + pub networks_client: NetworksClient, + pub tunnel_client: TunnelClient, +} + +impl BurrowClient { + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + pub async fn from_uds() -> Result { + let channel = Endpoint::try_from("http://[::]:50051")? // NOTE: this is a hack(?) + .connect_with_connector(service_fn(|_: Uri| async { + let sock_path = get_socket_path(); + Ok::<_, std::io::Error>(TokioIo::new(UnixStream::connect(sock_path).await?)) + })) + .await?; + let nw_client = NetworksClient::new(channel.clone()); + let tun_client = TunnelClient::new(channel.clone()); + Ok(BurrowClient { + networks_client: nw_client, + tunnel_client: tun_client, + }) + } +} diff --git a/burrow/src/daemon/rpc/grpc_defs.rs b/burrow/src/daemon/rpc/grpc_defs.rs new file mode 100644 index 0000000..f3085ee --- /dev/null +++ b/burrow/src/daemon/rpc/grpc_defs.rs @@ -0,0 +1,5 @@ +pub use burrowgrpc::*; + +mod burrowgrpc { + tonic::include_proto!("burrow"); +} diff --git a/burrow/src/daemon/rpc/mod.rs b/burrow/src/daemon/rpc/mod.rs index 4146e71..512662c 100644 --- a/burrow/src/daemon/rpc/mod.rs +++ b/burrow/src/daemon/rpc/mod.rs @@ -1,7 +1,10 @@ +pub mod client; +pub mod grpc_defs; pub mod notification; pub mod request; pub mod response; +pub use client::BurrowClient; pub use notification::DaemonNotification; pub use request::{DaemonCommand, DaemonRequest, DaemonStartOptions}; pub use response::{DaemonResponse, DaemonResponseData, ServerConfig, ServerInfo}; diff --git a/burrow/src/database.rs b/burrow/src/database.rs index 0047b01..9a9aac3 100644 --- a/burrow/src/database.rs +++ b/burrow/src/database.rs @@ -3,7 +3,15 @@ use std::path::Path; use anyhow::Result; use rusqlite::{params, Connection}; -use crate::wireguard::config::{Config, Interface, Peer}; +use crate::{ + daemon::rpc::grpc_defs::{ + Network as RPCNetwork, + NetworkDeleteRequest, + NetworkReorderRequest, + NetworkType, + }, + wireguard::config::{Config, Interface, Peer}, +}; #[cfg(target_vendor = "apple")] const DB_PATH: &str = "burrow.db"; @@ -30,8 +38,20 @@ const CREATE_WG_PEER_TABLE: &str = "CREATE TABLE IF NOT EXISTS wg_peer ( )"; const CREATE_NETWORK_TABLE: &str = "CREATE TABLE IF NOT EXISTS network ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + payload BLOB, + idx INTEGER, interface_id INT REFERENCES wg_interface(id) ON UPDATE CASCADE -)"; +); +CREATE TRIGGER IF NOT EXISTS increment_network_idx +AFTER INSERT ON network +BEGIN + UPDATE network + SET idx = (SELECT COALESCE(MAX(idx), 0) + 1 FROM network) + WHERE id = NEW.id; +END; +"; pub fn initialize_tables(conn: &Connection) -> Result<()> { conn.execute(CREATE_WG_INTERFACE_TABLE, [])?; @@ -40,20 +60,6 @@ pub fn initialize_tables(conn: &Connection) -> Result<()> { Ok(()) } -fn parse_lst(s: &str) -> Vec { - if s.is_empty() { - return vec![]; - } - s.split(',').map(|s| s.to_string()).collect() -} - -fn to_lst(v: &Vec) -> String { - v.iter() - .map(|s| s.to_string()) - .collect::>() - .join(",") -} - pub fn load_interface(conn: &Connection, interface_id: &str) -> Result { let iface = conn.query_row( "SELECT private_key, dns, address, listen_port, mtu FROM wg_interface WHERE id = ?", @@ -99,7 +105,7 @@ pub fn dump_interface(conn: &Connection, config: &Config) -> Result<()> { cif.private_key, to_lst(&cif.dns), to_lst(&cif.address), - cif.listen_port, + cif.listen_port.unwrap_or(51820), cif.mtu ])?; let interface_id = conn.last_insert_rowid(); @@ -127,10 +133,75 @@ pub fn get_connection(path: Option<&Path>) -> Result { Ok(Connection::open(p)?) } +pub fn add_network(conn: &Connection, network: &RPCNetwork) -> Result<()> { + let mut stmt = conn.prepare("INSERT INTO network (id, type, payload) VALUES (?, ?, ?)")?; + stmt.execute(params![ + network.id, + network.r#type().as_str_name(), + &network.payload + ])?; + if network.r#type() == NetworkType::WireGuard { + let payload_str = String::from_utf8(network.payload.clone())?; + let wg_config = Config::from_content_fmt(&payload_str, "ini")?; + dump_interface(conn, &wg_config)?; + } + Ok(()) +} + +pub fn list_networks(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT id, type, payload FROM network ORDER BY idx")?; + let networks: Vec = stmt + .query_map([], |row| { + println!("row: {:?}", row); + let network_id: i32 = row.get(0)?; + let network_type: String = row.get(1)?; + let network_type = NetworkType::from_str_name(network_type.as_str()) + .ok_or(rusqlite::Error::InvalidQuery)?; + let payload: Vec = row.get(2)?; + Ok(RPCNetwork { + id: network_id, + r#type: network_type.into(), + payload: payload.into(), + }) + })? + .collect::, rusqlite::Error>>()?; + Ok(networks) +} + +pub fn reorder_network(conn: &Connection, req: NetworkReorderRequest) -> Result<()> { + let mut stmt = conn.prepare("UPDATE network SET idx = ? WHERE id = ?")?; + let res = stmt.execute(params![req.index, req.id])?; + if res == 0 { + return Err(anyhow::anyhow!("No such network exists")); + } + Ok(()) +} + +pub fn delete_network(conn: &Connection, req: NetworkDeleteRequest) -> Result<()> { + let mut stmt = conn.prepare("DELETE FROM network WHERE id = ?")?; + let res = stmt.execute(params![req.id])?; + if res == 0 { + return Err(anyhow::anyhow!("No such network exists")); + } + Ok(()) +} + +fn parse_lst(s: &str) -> Vec { + if s.is_empty() { + return vec![]; + } + s.split(',').map(|s| s.to_string()).collect() +} + +fn to_lst(v: &Vec) -> String { + v.iter() + .map(|s| s.to_string()) + .collect::>() + .join(",") +} + #[cfg(test)] mod tests { - use std::path::Path; - use super::*; #[test] diff --git a/burrow/src/main.rs b/burrow/src/main.rs index ff07d4c..e87b4c9 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -11,8 +11,7 @@ mod wireguard; mod auth; #[cfg(any(target_os = "linux", target_vendor = "apple"))] -use daemon::{DaemonClient, DaemonCommand, DaemonStartOptions}; -use tun::TunOptions; +use daemon::{DaemonClient, DaemonCommand}; #[cfg(any(target_os = "linux", target_vendor = "apple"))] use crate::daemon::DaemonResponseData; @@ -20,6 +19,9 @@ use crate::daemon::DaemonResponseData; #[cfg(any(target_os = "linux", target_vendor = "apple"))] pub mod database; +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +use crate::daemon::rpc::{grpc_defs::Empty, BurrowClient}; + #[derive(Parser)] #[command(name = "Burrow")] #[command(author = "Hack Club ")] @@ -52,13 +54,24 @@ enum Commands { ReloadConfig(ReloadConfigArgs), /// Authentication server AuthServer, + /// Server Status + ServerStatus, + /// Tunnel Config + TunnelConfig, + /// Add Network + NetworkAdd(NetworkAddArgs), + /// List Networks + NetworkList, + /// Reorder Network + NetworkReorder(NetworkReorderArgs), + /// Delete Network + NetworkDelete(NetworkDeleteArgs), } #[derive(Args)] struct ReloadConfigArgs { #[clap(long, short)] interface_id: String, - } #[derive(Args)] @@ -67,21 +80,132 @@ struct StartArgs {} #[derive(Args)] struct DaemonArgs {} +#[derive(Args)] +struct NetworkAddArgs { + id: i32, + network_type: i32, + payload_path: String, +} + +#[derive(Args)] +struct NetworkReorderArgs { + id: i32, + index: i32, +} + +#[derive(Args)] +struct NetworkDeleteArgs { + id: i32, +} + #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_start() -> Result<()> { - let mut client = DaemonClient::new().await?; - client - .send_command(DaemonCommand::Start(DaemonStartOptions { - tun: TunOptions::new().address(vec!["10.13.13.2", "::2"]), - })) - .await - .map(|_| ()) + let mut client = BurrowClient::from_uds().await?; + let res = client.tunnel_client.tunnel_start(Empty {}).await?; + println!("Got results! {:?}", res); + Ok(()) } #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_stop() -> Result<()> { - let mut client = DaemonClient::new().await?; - client.send_command(DaemonCommand::Stop).await?; + let mut client = BurrowClient::from_uds().await?; + let res = client.tunnel_client.tunnel_stop(Empty {}).await?; + println!("Got results! {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_serverstatus() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .tunnel_client + .tunnel_status(Empty {}) + .await? + .into_inner(); + if let Some(st) = res.message().await? { + println!("Server Status: {:?}", st); + } else { + println!("Server Status is None"); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_tun_config() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .tunnel_client + .tunnel_configuration(Empty {}) + .await? + .into_inner(); + if let Some(config) = res.message().await? { + println!("Tunnel Config: {:?}", config); + } else { + println!("Tunnel Config is None"); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_add(id: i32, network_type: i32, payload_path: &str) -> Result<()> { + use tokio::{fs::File, io::AsyncReadExt}; + + use crate::daemon::rpc::grpc_defs::Network; + + let mut file = File::open(payload_path).await?; + let mut payload = Vec::new(); + file.read_to_end(&mut payload).await?; + + let mut client = BurrowClient::from_uds().await?; + let network = Network { + id, + r#type: network_type, + payload, + }; + let res = client.networks_client.network_add(network).await?; + println!("Network Add Response: {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_list() -> Result<()> { + let mut client = BurrowClient::from_uds().await?; + let mut res = client + .networks_client + .network_list(Empty {}) + .await? + .into_inner(); + while let Some(network_list) = res.message().await? { + println!("Network List: {:?}", network_list); + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_reorder(id: i32, index: i32) -> Result<()> { + use crate::daemon::rpc::grpc_defs::NetworkReorderRequest; + + let mut client = BurrowClient::from_uds().await?; + let reorder_request = NetworkReorderRequest { id, index }; + let res = client + .networks_client + .network_reorder(reorder_request) + .await?; + println!("Network Reorder Response: {:?}", res); + Ok(()) +} + +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_network_delete(id: i32) -> Result<()> { + use crate::daemon::rpc::grpc_defs::NetworkDeleteRequest; + + let mut client = BurrowClient::from_uds().await?; + let delete_request = NetworkDeleteRequest { id }; + let res = client + .networks_client + .network_delete(delete_request) + .await?; + println!("Network Delete Response: {:?}", res); Ok(()) } @@ -153,6 +277,14 @@ async fn main() -> Result<()> { Commands::ServerConfig => try_serverconfig().await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, Commands::AuthServer => crate::auth::server::serve().await?, + Commands::ServerStatus => try_serverstatus().await?, + Commands::TunnelConfig => try_tun_config().await?, + Commands::NetworkAdd(args) => { + try_network_add(args.id, args.network_type, &args.payload_path).await? + } + Commands::NetworkList => try_network_list().await?, + Commands::NetworkReorder(args) => try_network_reorder(args.id, args.index).await?, + Commands::NetworkDelete(args) => try_network_delete(args.id).await?, } Ok(()) diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index bd86a9f..5766675 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -3,9 +3,12 @@ use std::{net::ToSocketAddrs, str::FromStr}; use anyhow::{anyhow, Error, Result}; use base64::{engine::general_purpose, Engine}; use fehler::throws; +use ini::{Ini, Properties}; use ip_network::IpNetwork; +use serde::{Deserialize, Serialize}; use x25519_dalek::{PublicKey, StaticSecret}; +use super::inifield::IniField; use crate::wireguard::{Interface as WgInterface, Peer as WgPeer}; #[throws] @@ -31,7 +34,7 @@ fn parse_public_key(string: &str) -> PublicKey { /// A raw version of Peer Config that can be used later to reflect configuration files. /// This should be later converted to a `WgPeer`. /// Refers to https://github.com/pirate/wireguard-docs?tab=readme-ov-file#overview -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Peer { pub public_key: String, pub preshared_key: Option, @@ -41,17 +44,18 @@ pub struct Peer { pub name: Option, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Interface { pub private_key: String, pub address: Vec, - pub listen_port: u32, + pub listen_port: Option, pub dns: Vec, pub mtu: Option, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Config { + #[serde(rename = "Peer")] pub peers: Vec, pub interface: Interface, // Support for multiple interfaces? } @@ -98,7 +102,7 @@ impl Default for Config { interface: Interface { private_key: "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=".into(), address: vec!["10.13.13.2/24".into()], - listen_port: 51820, + listen_port: Some(51820), dns: Default::default(), mtu: Default::default(), }, @@ -113,3 +117,83 @@ impl Default for Config { } } } + +fn props_get(props: &Properties, key: &str) -> Result +where + T: TryFrom, +{ + IniField::try_from(props.get(key))?.try_into() +} + +impl TryFrom<&Properties> for Interface { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + private_key: props_get(props, "PrivateKey")?, + address: props_get(props, "Address")?, + listen_port: props_get(props, "ListenPort")?, + dns: props_get(props, "DNS")?, + mtu: props_get(props, "MTU")?, + }) + } +} + +impl TryFrom<&Properties> for Peer { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + public_key: props_get(props, "PublicKey")?, + preshared_key: props_get(props, "PresharedKey")?, + allowed_ips: props_get(props, "AllowedIPs")?, + endpoint: props_get(props, "Endpoint")?, + persistent_keepalive: props_get(props, "PersistentKeepalive")?, + name: props_get(props, "Name")?, + }) + } +} + +impl Config { + pub fn from_toml(toml: &str) -> Result { + toml::from_str(toml).map_err(Into::into) + } + + pub fn from_ini(ini: &str) -> Result { + let ini = Ini::load_from_str(ini)?; + let interface = ini + .section(Some("Interface")) + .ok_or(anyhow!("Interface section not found"))?; + let peers = ini.section_all(Some("Peer")); + Ok(Self { + interface: Interface::try_from(interface)?, + peers: peers + .into_iter() + .map(|v| Peer::try_from(v)) + .collect::>>()?, + }) + } + + pub fn from_content_fmt(content: &str, fmt: &str) -> Result { + match fmt { + "toml" => Self::from_toml(content), + "ini" | "conf" => Self::from_ini(content), + _ => Err(anyhow::anyhow!("Unsupported format: {}", fmt)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tst_config_toml() { + let cfig = Config::default(); + let toml = toml::to_string(&cfig).unwrap(); + println!("{}", &toml); + insta::assert_snapshot!(toml); + let cfig2: Config = toml::from_str(&toml).unwrap(); + assert_eq!(cfig, cfig2); + } +} diff --git a/burrow/src/wireguard/iface.rs b/burrow/src/wireguard/iface.rs index 84b5489..321801b 100755 --- a/burrow/src/wireguard/iface.rs +++ b/burrow/src/wireguard/iface.rs @@ -93,6 +93,12 @@ impl Interface { *st = IfaceStatus::Running; } + pub async fn set_tun_ref(&mut self, tun: Arc>>) { + self.tun = tun; + let mut st = self.status.write().await; + *st = IfaceStatus::Running; + } + pub fn get_tun(&self) -> Arc>> { self.tun.clone() } @@ -135,7 +141,7 @@ impl Interface { Some(addr) => addr, None => { debug!("No destination found"); - continue + continue; } }; @@ -154,7 +160,7 @@ impl Interface { } Err(e) => { log::error!("Failed to send packet {}", e); - continue + continue; } }; } @@ -175,7 +181,7 @@ impl Interface { let main_tsk = async move { if let Err(e) = pcb.open_if_closed().await { log::error!("failed to open pcb: {}", e); - return + return; } let r2 = pcb.run(tun).await; if let Err(e) = r2 { @@ -195,7 +201,7 @@ impl Interface { Ok(..) => (), Err(e) => { error!("Failed to update timers: {}", e); - return + return; } } } diff --git a/burrow/src/wireguard/inifield.rs b/burrow/src/wireguard/inifield.rs new file mode 100644 index 0000000..946868d --- /dev/null +++ b/burrow/src/wireguard/inifield.rs @@ -0,0 +1,81 @@ +use std::str::FromStr; + +use anyhow::{Error, Result}; + +pub struct IniField(String); + +impl FromStr for IniField { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +impl TryFrom for Vec { + type Error = Error; + + fn try_from(field: IniField) -> Result { + Ok(field.0.split(',').map(|s| s.trim().to_string()).collect()) + } +} + +impl TryFrom for u32 { + type Error = Error; + + fn try_from(value: IniField) -> Result { + value.0.parse().map_err(Error::from) + } +} + +impl TryFrom for Option { + type Error = Error; + + fn try_from(value: IniField) -> Result { + if value.0.is_empty() { + Ok(None) + } else { + value.0.parse().map(Some).map_err(Error::from) + } + } +} + +impl TryFrom for String { + type Error = Error; + + fn try_from(value: IniField) -> Result { + Ok(value.0) + } +} + +impl TryFrom for Option { + type Error = Error; + + fn try_from(value: IniField) -> Result { + if value.0.is_empty() { + Ok(None) + } else { + Ok(Some(value.0)) + } + } +} + +impl TryFrom> for IniField +where + T: ToString, +{ + type Error = Error; + + fn try_from(value: Option) -> Result { + Ok(match value { + Some(v) => Self(v.to_string()), + None => Self(String::new()), + }) + } +} + +impl IniField { + fn new(value: &str) -> Self { + Self(value.to_string()) + } +} diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index 4c70a7f..cfb4585 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -1,5 +1,6 @@ pub mod config; mod iface; +mod inifield; mod noise; mod pcb; mod peer; diff --git a/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap b/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap new file mode 100644 index 0000000..3800647 --- /dev/null +++ b/burrow/src/wireguard/snapshots/burrow__wireguard__config__tests__tst_config_toml.snap @@ -0,0 +1,16 @@ +--- +source: burrow/src/wireguard/config.rs +expression: toml +--- +[[Peer]] +public_key = "8GaFjVO6c4luCHG4ONO+1bFG8tO+Zz5/Gy+Geht1USM=" +preshared_key = "ha7j4BjD49sIzyF9SNlbueK0AMHghlj6+u0G3bzC698=" +allowed_ips = ["8.8.8.8/32", "0.0.0.0/0"] +endpoint = "wg.burrow.rs:51820" + +[interface] +private_key = "OEPVdomeLTxTIBvv3TYsJRge0Hp9NMiY0sIrhT8OWG8=" +address = ["10.13.13.2/24"] +listen_port = 51820 +dns = [] + diff --git a/burrow/tmp/conrd.conf b/burrow/tmp/conrd.conf new file mode 100644 index 0000000..52572d1 --- /dev/null +++ b/burrow/tmp/conrd.conf @@ -0,0 +1,8 @@ +[Interface] +PrivateKey = gAaK0KFGOpxY7geGo59XXDufcxeoSNXXNC12mCQmlVs= +Address = 10.1.11.2/32 +DNS = 10.1.11.1 +[Peer] +PublicKey = Ab6V2mgPHiCXaAZfQrNts8ha8RkEzC49VnmMQfe5Yg4= +AllowedIPs = 10.1.11.1/32,10.1.11.2/32,0.0.0.0/0 +Endpoint = 172.251.163.175:51820 \ No newline at end of file diff --git a/proto/burrow.proto b/proto/burrow.proto index 2d29c78..2355b8d 100644 --- a/proto/burrow.proto +++ b/proto/burrow.proto @@ -11,7 +11,7 @@ service Tunnel { } service Networks { - rpc NetworkAdd (Empty) returns (Empty); + rpc NetworkAdd (Network) returns (Empty); rpc NetworkList (Empty) returns (stream NetworkListResponse); rpc NetworkReorder (NetworkReorderRequest) returns (Empty); rpc NetworkDelete (NetworkDeleteRequest) returns (Empty); From 25a0f7c42158831ceb5f6bbe7defe3c067eb586c Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 7 Sep 2024 20:35:28 -0700 Subject: [PATCH 24/25] Add Developer ID Profiles to build --- .github/workflows/release-apple.yml | 5 +++++ .../Burrow_Developer_ID.provisionprofile | Bin 0 -> 13091 bytes ...Burrow_Network_Developer_ID.provisionprofile | Bin 0 -> 13027 bytes 3 files changed, 5 insertions(+) create mode 100644 Apple/Profiles/Burrow_Developer_ID.provisionprofile create mode 100644 Apple/Profiles/Burrow_Network_Developer_ID.provisionprofile diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index bb9c15a..c0a34a9 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -38,6 +38,11 @@ jobs: app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} + - name: Install Provisioning Profiles + shell: bash + run: | + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/ + cp -f Apple/Profiles/* ~/Library/MobileDevice/Provisioning\ Profiles/ - name: Install Rust uses: dtolnay/rust-toolchain@stable with: diff --git a/Apple/Profiles/Burrow_Developer_ID.provisionprofile b/Apple/Profiles/Burrow_Developer_ID.provisionprofile new file mode 100644 index 0000000000000000000000000000000000000000..3ecd831fe2614bb8b5aea636ca5c080a48d99a98 GIT binary patch literal 13091 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 Uf<>W=OI4qBvCm4;3e_kF0D*cnOaK4? literal 0 HcmV?d00001 From 85640ffce18eac6ac1b6fa85ff278a457c955198 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sat, 13 Jul 2024 18:08:43 -0700 Subject: [PATCH 25/25] Switch to gRPC client in Swift app --- .github/workflows/build-apple.yml | 9 +- .github/workflows/release-apple.yml | 4 + .gitignore | 3 + .swiftlint.yml | 1 - Apple/App/AppDelegate.swift | 1 + Apple/App/BurrowApp.swift | 3 +- Apple/App/MainMenu.xib | 4 +- Apple/App/Networks/Network.swift | 10 - Apple/App/Tunnel.swift | 50 - Apple/Burrow.xcodeproj/project.pbxproj | 814 +++++++++---- .../xcshareddata/swiftpm/Package.resolved | 123 ++ .../xcshareddata/xcschemes/App.xcscheme | 5 +- .../xcschemes/NetworkExtension.xcscheme | 6 +- Apple/Configuration/App.xcconfig | 6 +- Apple/Configuration/Compiler.xcconfig | 41 +- .../Configuration.xcconfig} | 5 +- .../Constants/Constants.h | 0 .../Constants}/Constants.swift | 31 +- .../Constants/module.modulemap | 2 +- Apple/Configuration/Debug.xcconfig | 26 + Apple/Configuration/Extension.xcconfig | 6 +- Apple/Configuration/Framework.xcconfig | 14 + Apple/Core/Client.swift | 32 + Apple/Core/Client/burrow.proto | 1 + Apple/Core/Client/grpc-swift-config.json | 11 + Apple/Core/Client/swift-protobuf-config.json | 10 + Apple/{Shared => Core}/Logging.swift | 2 +- .../PacketTunnelProvider.swift | 108 +- .../NetworkExtension/libburrow/build-rust.sh | 5 +- Apple/NetworkExtension/libburrow/libburrow.h | 2 +- Apple/Shared/Client.swift | 106 -- Apple/Shared/DataTypes.swift | 139 --- Apple/Shared/NWConnection+Async.swift | 32 - Apple/Shared/NewlineProtocolFramer.swift | 54 - .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/128.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../Assets.xcassets/AppIcon.appiconset/16.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/172.png | Bin .../AppIcon.appiconset/180.png | Bin .../AppIcon.appiconset/196.png | Bin .../Assets.xcassets/AppIcon.appiconset/20.png | Bin .../AppIcon.appiconset/216.png | Bin .../AppIcon.appiconset/256.png | Bin .../Assets.xcassets/AppIcon.appiconset/29.png | Bin .../Assets.xcassets/AppIcon.appiconset/32.png | Bin .../Assets.xcassets/AppIcon.appiconset/40.png | Bin .../Assets.xcassets/AppIcon.appiconset/48.png | Bin .../Assets.xcassets/AppIcon.appiconset/50.png | Bin .../AppIcon.appiconset/512.png | Bin .../Assets.xcassets/AppIcon.appiconset/55.png | Bin .../Assets.xcassets/AppIcon.appiconset/57.png | Bin .../Assets.xcassets/AppIcon.appiconset/58.png | Bin .../Assets.xcassets/AppIcon.appiconset/60.png | Bin .../Assets.xcassets/AppIcon.appiconset/64.png | Bin .../Assets.xcassets/AppIcon.appiconset/72.png | Bin .../Assets.xcassets/AppIcon.appiconset/76.png | Bin .../Assets.xcassets/AppIcon.appiconset/80.png | Bin .../Assets.xcassets/AppIcon.appiconset/87.png | Bin .../Assets.xcassets/AppIcon.appiconset/88.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../{App => UI}/Assets.xcassets/Contents.json | 0 .../HackClub.colorset/Contents.json | 0 .../HackClub.imageset/Contents.json | 0 .../flag-standalone-wtransparent.pdf | Bin .../WireGuard.colorset/Contents.json | 0 .../WireGuard.imageset/Contents.json | 0 .../WireGuard.imageset/WireGuard.svg | 0 .../WireGuardTitle.imageset/Contents.json | 0 .../WireGuardTitle.svg | 0 Apple/{App => UI}/BurrowView.swift | 7 +- Apple/{App => UI}/FloatingButtonStyle.swift | 0 Apple/{App => UI}/MenuItemToggleView.swift | 11 +- Apple/{App => UI}/NetworkCarouselView.swift | 8 +- .../{App => UI}/NetworkExtension+Async.swift | 6 +- .../{App => UI}/NetworkExtensionTunnel.swift | 72 +- Apple/{App => UI}/NetworkView.swift | 0 Apple/{App => UI}/Networks/HackClub.swift | 8 +- Apple/UI/Networks/Network.swift | 36 + Apple/{App => UI}/Networks/WireGuard.swift | 8 +- Apple/{App => UI}/OAuth2.swift | 21 +- Apple/UI/Tunnel.swift | 61 + Apple/{App => UI}/TunnelButton.swift | 2 +- Apple/{App => UI}/TunnelStatusView.swift | 2 +- Apple/UI/UI.xcconfig | 3 + Cargo.lock | 1080 +++++++++-------- burrow-gtk/build-aux/Dockerfile | 2 +- 93 files changed, 1666 insertions(+), 1327 deletions(-) delete mode 100644 Apple/App/Networks/Network.swift delete mode 100644 Apple/App/Tunnel.swift create mode 100644 Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Apple/{Shared/Shared.xcconfig => Configuration/Configuration.xcconfig} (65%) rename Apple/{Shared => Configuration}/Constants/Constants.h (100%) rename Apple/{Shared => Configuration/Constants}/Constants.swift (61%) rename Apple/{Shared => Configuration}/Constants/module.modulemap (66%) create mode 100644 Apple/Configuration/Debug.xcconfig create mode 100644 Apple/Configuration/Framework.xcconfig create mode 100644 Apple/Core/Client.swift create mode 120000 Apple/Core/Client/burrow.proto create mode 100644 Apple/Core/Client/grpc-swift-config.json create mode 100644 Apple/Core/Client/swift-protobuf-config.json rename Apple/{Shared => Core}/Logging.swift (88%) delete mode 100644 Apple/Shared/Client.swift delete mode 100644 Apple/Shared/DataTypes.swift delete mode 100644 Apple/Shared/NWConnection+Async.swift delete mode 100644 Apple/Shared/NewlineProtocolFramer.swift rename Apple/{App => UI}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/100.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/114.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/120.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/128.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/144.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/152.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/16.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/167.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/172.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/180.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/196.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/20.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/216.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/256.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/29.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/32.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/40.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/48.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/50.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/512.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/55.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/57.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/58.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/60.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/64.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/72.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/76.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/80.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/87.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/88.png (100%) rename Apple/{App => UI}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.colorset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuard.imageset/WireGuard.svg (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuardTitle.imageset/Contents.json (100%) rename Apple/{App => UI}/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg (100%) rename Apple/{App => UI}/BurrowView.swift (95%) rename Apple/{App => UI}/FloatingButtonStyle.swift (100%) rename Apple/{App => UI}/MenuItemToggleView.swift (87%) rename Apple/{App => UI}/NetworkCarouselView.swift (90%) rename Apple/{App => UI}/NetworkExtension+Async.swift (82%) rename Apple/{App => UI}/NetworkExtensionTunnel.swift (67%) rename Apple/{App => UI}/NetworkView.swift (100%) rename Apple/{App => UI}/Networks/HackClub.swift (76%) create mode 100644 Apple/UI/Networks/Network.swift rename Apple/{App => UI}/Networks/WireGuard.swift (82%) rename Apple/{App => UI}/OAuth2.swift (94%) create mode 100644 Apple/UI/Tunnel.swift rename Apple/{App => UI}/TunnelButton.swift (95%) rename Apple/{App => UI}/TunnelStatusView.swift (95%) create mode 100644 Apple/UI/UI.xcconfig diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index b628001..7ae8c4c 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -39,7 +39,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer - PROTOC_VERSION: 3.25.1 + PROTOC_PATH: /opt/homebrew/bin/protoc steps: - name: Checkout uses: actions/checkout@v3 @@ -55,10 +55,9 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} - - name: Install protoc - uses: taiki-e/install-action@v2 - with: - tool: protoc@${{ env.PROTOC_VERSION }} + - name: Install Protobuf + shell: bash + run: brew install protobuf - name: Build id: build uses: ./.github/actions/build-for-testing diff --git a/.github/workflows/release-apple.yml b/.github/workflows/release-apple.yml index c0a34a9..c869d6a 100644 --- a/.github/workflows/release-apple.yml +++ b/.github/workflows/release-apple.yml @@ -22,6 +22,7 @@ jobs: - aarch64-apple-darwin env: DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer + PROTOC_PATH: /opt/homebrew/bin/protoc steps: - name: Checkout uses: actions/checkout@v4 @@ -47,6 +48,9 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.rust-targets, ', ') }} + - name: Install Protobuf + shell: bash + run: brew install protobuf - name: Configure Version id: version shell: bash diff --git a/.gitignore b/.gitignore index 997d4d5..1b300b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Xcode xcuserdata +# Swift +Apple/Package/.swiftpm/ + # Rust target/ .env diff --git a/.swiftlint.yml b/.swiftlint.yml index 22ef035..8efc85e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -30,7 +30,6 @@ opt_in_rules: - function_default_parameter_at_end - ibinspectable_in_extension - identical_operands -- implicitly_unwrapped_optional - indentation_width - joined_default_parameter - last_where diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index b0c5546..0ea93f4 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -1,5 +1,6 @@ #if os(macOS) import AppKit +import BurrowUI import SwiftUI @main diff --git a/Apple/App/BurrowApp.swift b/Apple/App/BurrowApp.swift index 21ebf84..838ef54 100644 --- a/Apple/App/BurrowApp.swift +++ b/Apple/App/BurrowApp.swift @@ -1,6 +1,7 @@ +#if !os(macOS) +import BurrowUI import SwiftUI -#if !os(macOS) @MainActor @main struct BurrowApp: App { diff --git a/Apple/App/MainMenu.xib b/Apple/App/MainMenu.xib index 587f6c4..50ba431 100644 --- a/Apple/App/MainMenu.xib +++ b/Apple/App/MainMenu.xib @@ -1,7 +1,7 @@ - + - + diff --git a/Apple/App/Networks/Network.swift b/Apple/App/Networks/Network.swift deleted file mode 100644 index d441d24..0000000 --- a/Apple/App/Networks/Network.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -protocol Network { - associatedtype Label: View - - var id: String { get } - var backgroundColor: Color { get } - - var label: Label { get } -} diff --git a/Apple/App/Tunnel.swift b/Apple/App/Tunnel.swift deleted file mode 100644 index 8db366f..0000000 --- a/Apple/App/Tunnel.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI - -protocol Tunnel { - var status: TunnelStatus { get } - - func start() - func stop() - func enable() -} - -enum TunnelStatus: Equatable, Hashable { - case unknown - case permissionRequired - case disabled - case connecting - case connected(Date) - case disconnecting - case disconnected - case reasserting - case invalid - case configurationReadWriteFailed -} - -struct TunnelKey: EnvironmentKey { - static let defaultValue: any Tunnel = NetworkExtensionTunnel() -} - -extension EnvironmentValues { - var tunnel: any Tunnel { - get { self[TunnelKey.self] } - set { self[TunnelKey.self] = newValue } - } -} - -#if DEBUG -@Observable -class PreviewTunnel: Tunnel { - var status: TunnelStatus = .permissionRequired - - func start() { - status = .connected(.now) - } - func stop() { - status = .disconnected - } - func enable() { - status = .disconnected - } -} -#endif diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 5c5e80b..617b88f 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -7,52 +7,50 @@ objects = { /* Begin PBXBuildFile section */ - 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46E8DF2AC918CA00BA2A3C /* Client.swift */; }; - 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */; }; - 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */; }; - 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B28F1552ABF463A000D44B0 /* DataTypes.swift */; }; - 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */; }; - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */; }; - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D000363E2BB895FB00E582EC /* OAuth2.swift */; }; - D001173B2B30341C00D87C25 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001173A2B30341C00D87C25 /* Logging.swift */; }; - D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; - D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D00117382B30341C00D87C25 /* libBurrowShared.a */; }; D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00AA8962A4669BC005C8102 /* AppDelegate.swift */; }; - D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A79302B81630D0024EC91 /* NetworkView.swift */; }; D020F65829E4A697002790F6 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F65729E4A697002790F6 /* PacketTunnelProvider.swift */; }; D020F65D29E4A697002790F6 /* BurrowNetworkExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6512B8A79C20006B8AD /* HackClub.swift */; }; - D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032E6532B8A79DA0006B8AD /* WireGuard.swift */; }; + D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E22C8DA375008A8CEC /* SwiftProtobuf */; }; + D03383AE2C8E67E300F7C44E /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE902C8DAB2000778185 /* NIO */; }; + D03383AF2C8E67E300F7C44E /* NIOConcurrencyHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */; }; + D03383B02C8E67E300F7C44E /* NIOTransportServices in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE952C8DAB2800778185 /* NIOTransportServices */; }; D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; }; - D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */; }; - D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D05B9F7929E39EED008CB1F9 /* Assets.xcassets */; }; - D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */; }; - D08252762B5C9FC4005DA378 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08252752B5C9FC4005DA378 /* Constants.swift */; }; - D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; }; - D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */; }; - D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B98FC629FDC5B5004E7149 /* Tunnel.swift */; }; + D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; platformFilters = (macos, ); }; + D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = D0B1D10F2C436152004B7823 /* AsyncAlgorithms */; }; D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; }; - D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */; }; - D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5952B818B2900F6A84B /* TunnelButton.swift */; }; - D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */; }; - D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FAB5992B818B9600F6A84B /* Network.swift */; }; + D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; }; + D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; }; + D0D4E53A2C8D996F007F820A /* BurrowCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49A2C8D921A007F820A /* Logging.swift */; }; + D0D4E5702C8D9C62007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0D4E5712C8D9C6F007F820A /* HackClub.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49D2C8D921A007F820A /* HackClub.swift */; }; + D0D4E5722C8D9C6F007F820A /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49E2C8D921A007F820A /* Network.swift */; }; + D0D4E5732C8D9C6F007F820A /* WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E49F2C8D921A007F820A /* WireGuard.swift */; }; + D0D4E5742C8D9C6F007F820A /* BurrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A22C8D921A007F820A /* BurrowView.swift */; }; + D0D4E5752C8D9C6F007F820A /* FloatingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */; }; + D0D4E5762C8D9C6F007F820A /* MenuItemToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */; }; + D0D4E5772C8D9C6F007F820A /* NetworkCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */; }; + D0D4E5782C8D9C6F007F820A /* NetworkExtension+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */; }; + D0D4E5792C8D9C6F007F820A /* NetworkExtensionTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */; }; + D0D4E57A2C8D9C6F007F820A /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A82C8D921A007F820A /* NetworkView.swift */; }; + D0D4E57B2C8D9C6F007F820A /* OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4A92C8D921A007F820A /* OAuth2.swift */; }; + D0D4E57C2C8D9C6F007F820A /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AA2C8D921A007F820A /* Tunnel.swift */; }; + D0D4E57D2C8D9C6F007F820A /* TunnelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */; }; + D0D4E57E2C8D9C6F007F820A /* TunnelStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */; }; + D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; }; + D0D4E58A2C8D9C9E007F820A /* BurrowUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E58B2C8D9CA4007F820A /* BurrowConfiguration.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0D4E5922C8D9D15007F820A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E58F2C8D9D0A007F820A /* Constants.swift */; }; + D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; + D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E02C8DA375008A8CEC /* GRPC */; }; + D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4962C8D921A007F820A /* grpc-swift-config.json */; }; + D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */; }; + D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */ = {isa = PBXBuildFile; productRef = D0F7597D2C8DB30500126CF3 /* CGRPCZlib */; }; + D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4992C8D921A007F820A /* Client.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - D00117462B30373100D87C25 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D00117372B30341C00D87C25; - remoteInfo = Shared; - }; - D00117482B30373500D87C25 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D00117372B30341C00D87C25; - remoteInfo = Shared; - }; D020F65B29E4A697002790F6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; @@ -60,6 +58,48 @@ remoteGlobalIDString = D020F65229E4A697002790F6; remoteInfo = BurrowNetworkExtension; }; + D0BF09502C8E66F1000D8DEC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E55A2C8D9BF4007F820A; + remoteInfo = Configuration; + }; + D0BF09532C8E66FA000D8DEC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E55A2C8D9BF4007F820A; + remoteInfo = Configuration; + }; + D0D4E56E2C8D9C5D007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; + D0D4E57F2C8D9C78007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; + D0D4E5872C8D9C88007F820A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5502C8D9BF2007F820A; + remoteInfo = UI; + }; + D0F4FAD12C8DC7960068730A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D05B9F6A29E39EEC008CB1F9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D4E5302C8D996F007F820A; + remoteInfo = Core; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -74,22 +114,24 @@ name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + D0D4E53F2C8D996F007F820A /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D0D4E58B2C8D9CA4007F820A /* BurrowConfiguration.framework in Embed Frameworks */, + D0D4E58A2C8D9C9E007F820A /* BurrowUI.framework in Embed Frameworks */, + D0D4E53A2C8D996F007F820A /* BurrowCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypes.swift; sourceTree = ""; }; - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; - 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; - D000363E2BB895FB00E582EC /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWConnection+Async.swift"; sourceTree = ""; }; - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewlineProtocolFramer.swift; sourceTree = ""; }; - D00117382B30341C00D87C25 /* libBurrowShared.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBurrowShared.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D001173A2B30341C00D87C25 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; - D00117412B30347800D87C25 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - D00117422B30348D00D87C25 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; + D00117422B30348D00D87C25 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Configuration.xcconfig; sourceTree = ""; }; D00AA8962A4669BC005C8102 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D01A79302B81630D0024EC91 /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; D020F63D29E4A1FF002790F6 /* Identity.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Identity.xcconfig; sourceTree = ""; }; D020F64029E4A1FF002790F6 /* Compiler.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Compiler.xcconfig; sourceTree = ""; }; D020F64229E4A1FF002790F6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -104,43 +146,54 @@ D020F66729E4A95D002790F6 /* NetworkExtension-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "NetworkExtension-iOS.entitlements"; sourceTree = ""; }; D020F66829E4AA74002790F6 /* App-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-iOS.entitlements"; sourceTree = ""; }; D020F66929E4AA74002790F6 /* App-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "App-macOS.entitlements"; sourceTree = ""; }; - D032E6512B8A79C20006B8AD /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; - D032E6532B8A79DA0006B8AD /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; D05B9F7229E39EEC008CB1F9 /* Burrow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Burrow.app; sourceTree = BUILT_PRODUCTS_DIR; }; D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowApp.swift; sourceTree = ""; }; - D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; - D05B9F7929E39EED008CB1F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; - D08252742B5C9DEB005DA378 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; - D08252752B5C9FC4005DA378 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D09150412B9D2AF700BE3CB0 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; D0B98FBF29FD8072004E7149 /* build-rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; - D0B98FC629FDC5B5004E7149 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; D0B98FD829FDDB6F004E7149 /* libburrow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libburrow.h; sourceTree = ""; }; D0B98FDC29FDDDCF004E7149 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; - D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; - D0FAB5952B818B2900F6A84B /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; - D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; - D0FAB5992B818B9600F6A84B /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + D0BF09582C8E6789000D8DEC /* UI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UI.xcconfig; sourceTree = ""; }; + D0D4E4952C8D921A007F820A /* burrow.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = burrow.proto; sourceTree = ""; }; + D0D4E4962C8D921A007F820A /* grpc-swift-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "grpc-swift-config.json"; sourceTree = ""; }; + D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "swift-protobuf-config.json"; sourceTree = ""; }; + D0D4E4992C8D921A007F820A /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; + D0D4E49A2C8D921A007F820A /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + D0D4E49D2C8D921A007F820A /* HackClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackClub.swift; sourceTree = ""; }; + D0D4E49E2C8D921A007F820A /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + D0D4E49F2C8D921A007F820A /* WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuard.swift; sourceTree = ""; }; + D0D4E4A12C8D921A007F820A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D0D4E4A22C8D921A007F820A /* BurrowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurrowView.swift; sourceTree = ""; }; + D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingButtonStyle.swift; sourceTree = ""; }; + D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemToggleView.swift; sourceTree = ""; }; + D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCarouselView.swift; sourceTree = ""; }; + D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkExtension+Async.swift"; sourceTree = ""; }; + D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkExtensionTunnel.swift; sourceTree = ""; }; + D0D4E4A82C8D921A007F820A /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; + D0D4E4A92C8D921A007F820A /* OAuth2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2.swift; sourceTree = ""; }; + D0D4E4AA2C8D921A007F820A /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; + D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelButton.swift; sourceTree = ""; }; + D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusView.swift; sourceTree = ""; }; + D0D4E4F62C8D932D007F820A /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + D0D4E4F72C8D941D007F820A /* Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Framework.xcconfig; sourceTree = ""; }; + D0D4E5312C8D996F007F820A /* BurrowCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BurrowConfiguration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D4E58E2C8D9D0A007F820A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + D0D4E58F2C8D9D0A007F820A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + D0D4E5902C8D9D0A007F820A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - D00117352B30341C00D87C25 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D020F65029E4A697002790F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D00117442B30372900D87C25 /* libBurrowShared.a in Frameworks */, + D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */, + D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */, D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */, + D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -148,37 +201,36 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D00117452B30372C00D87C25 /* libBurrowShared.a in Frameworks */, + D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */, + D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */, + D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D078F7CF2C8DA213008A8CEC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D03383B02C8E67E300F7C44E /* NIOTransportServices in Frameworks */, + D03383AF2C8E67E300F7C44E /* NIOConcurrencyHelpers in Frameworks */, + D03383AE2C8E67E300F7C44E /* NIO in Frameworks */, + D03383AD2C8E67E300F7C44E /* SwiftProtobuf in Frameworks */, + D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */, + D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E5532C8D9BF2007F820A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5702C8D9C62007F820A /* BurrowCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - D00117392B30341C00D87C25 /* Shared */ = { - isa = PBXGroup; - children = ( - 0B28F1552ABF463A000D44B0 /* DataTypes.swift */, - D00117322B3001A400D87C25 /* NewlineProtocolFramer.swift */, - D00117302B2FFFC900D87C25 /* NWConnection+Async.swift */, - 0B46E8DF2AC918CA00BA2A3C /* Client.swift */, - D001173A2B30341C00D87C25 /* Logging.swift */, - D08252752B5C9FC4005DA378 /* Constants.swift */, - D00117422B30348D00D87C25 /* Shared.xcconfig */, - D001173F2B30347800D87C25 /* Constants */, - ); - path = Shared; - sourceTree = ""; - }; - D001173F2B30347800D87C25 /* Constants */ = { - isa = PBXGroup; - children = ( - D08252742B5C9DEB005DA378 /* Constants.h */, - D00117412B30347800D87C25 /* module.modulemap */, - ); - path = Constants; - sourceTree = ""; - }; D00117432B30372900D87C25 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -192,9 +244,13 @@ D020F63D29E4A1FF002790F6 /* Identity.xcconfig */, D020F64A29E4A452002790F6 /* App.xcconfig */, D020F66329E4A703002790F6 /* Extension.xcconfig */, + D0D4E4F72C8D941D007F820A /* Framework.xcconfig */, D020F64029E4A1FF002790F6 /* Compiler.xcconfig */, + D0D4E4F62C8D932D007F820A /* Debug.xcconfig */, D04A3E1D2BAF465F0043EC85 /* Version.xcconfig */, D020F64229E4A1FF002790F6 /* Info.plist */, + D0D4E5912C8D9D0A007F820A /* Constants */, + D00117422B30348D00D87C25 /* Configuration.xcconfig */, ); path = Configuration; sourceTree = ""; @@ -212,22 +268,13 @@ path = NetworkExtension; sourceTree = ""; }; - D032E64D2B8A69C90006B8AD /* Networks */ = { - isa = PBXGroup; - children = ( - D0FAB5992B818B9600F6A84B /* Network.swift */, - D032E6512B8A79C20006B8AD /* HackClub.swift */, - D032E6532B8A79DA0006B8AD /* WireGuard.swift */, - ); - path = Networks; - sourceTree = ""; - }; D05B9F6929E39EEC008CB1F9 = { isa = PBXGroup; children = ( D05B9F7429E39EEC008CB1F9 /* App */, D020F65629E4A697002790F6 /* NetworkExtension */, - D00117392B30341C00D87C25 /* Shared */, + D0D4E49C2C8D921A007F820A /* Core */, + D0D4E4AD2C8D921A007F820A /* UI */, D020F63C29E4A1FF002790F6 /* Configuration */, D05B9F7329E39EEC008CB1F9 /* Products */, D00117432B30372900D87C25 /* Frameworks */, @@ -239,7 +286,10 @@ children = ( D05B9F7229E39EEC008CB1F9 /* Burrow.app */, D020F65329E4A697002790F6 /* BurrowNetworkExtension.appex */, - D00117382B30341C00D87C25 /* libBurrowShared.a */, + D0BCC6032A09535900AD070D /* libburrow.a */, + D0D4E5312C8D996F007F820A /* BurrowCore.framework */, + D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */, + D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */, ); name = Products; sourceTree = ""; @@ -249,19 +299,6 @@ children = ( D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */, D00AA8962A4669BC005C8102 /* AppDelegate.swift */, - 43AA26D72A10004900F14CE6 /* MenuItemToggleView.swift */, - D05B9F7729E39EEC008CB1F9 /* BurrowView.swift */, - D000363C2BB8928E00E582EC /* NetworkCarouselView.swift */, - D01A79302B81630D0024EC91 /* NetworkView.swift */, - D000363E2BB895FB00E582EC /* OAuth2.swift */, - D032E64D2B8A69C90006B8AD /* Networks */, - D0FAB5972B818B8200F6A84B /* TunnelStatusView.swift */, - D0FAB5952B818B2900F6A84B /* TunnelButton.swift */, - D0B98FC629FDC5B5004E7149 /* Tunnel.swift */, - D0FAB5912B818A5900F6A84B /* NetworkExtensionTunnel.swift */, - D0BCC5FC2A086D4700AD070D /* NetworkExtension+Async.swift */, - D05EF8C72B81818D0017AB4F /* FloatingButtonStyle.swift */, - D05B9F7929E39EED008CB1F9 /* Assets.xcassets */, D09150412B9D2AF700BE3CB0 /* MainMenu.xib */, D020F66829E4AA74002790F6 /* App-iOS.entitlements */, D020F66929E4AA74002790F6 /* App-macOS.entitlements */, @@ -276,30 +313,74 @@ D0B98FBF29FD8072004E7149 /* build-rust.sh */, D0B98FDC29FDDDCF004E7149 /* module.modulemap */, D0B98FD829FDDB6F004E7149 /* libburrow.h */, - D0BCC6032A09535900AD070D /* libburrow.a */, ); path = libburrow; sourceTree = ""; }; + D0D4E4982C8D921A007F820A /* Client */ = { + isa = PBXGroup; + children = ( + D0D4E4952C8D921A007F820A /* burrow.proto */, + D0D4E4962C8D921A007F820A /* grpc-swift-config.json */, + D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */, + ); + path = Client; + sourceTree = ""; + }; + D0D4E49C2C8D921A007F820A /* Core */ = { + isa = PBXGroup; + children = ( + D0D4E49A2C8D921A007F820A /* Logging.swift */, + D0D4E4992C8D921A007F820A /* Client.swift */, + D0D4E4982C8D921A007F820A /* Client */, + ); + path = Core; + sourceTree = ""; + }; + D0D4E4A02C8D921A007F820A /* Networks */ = { + isa = PBXGroup; + children = ( + D0D4E49D2C8D921A007F820A /* HackClub.swift */, + D0D4E49E2C8D921A007F820A /* Network.swift */, + D0D4E49F2C8D921A007F820A /* WireGuard.swift */, + ); + path = Networks; + sourceTree = ""; + }; + D0D4E4AD2C8D921A007F820A /* UI */ = { + isa = PBXGroup; + children = ( + D0D4E4A22C8D921A007F820A /* BurrowView.swift */, + D0D4E4A02C8D921A007F820A /* Networks */, + D0D4E4A32C8D921A007F820A /* FloatingButtonStyle.swift */, + D0D4E4A42C8D921A007F820A /* MenuItemToggleView.swift */, + D0D4E4A52C8D921A007F820A /* NetworkCarouselView.swift */, + D0D4E4A62C8D921A007F820A /* NetworkExtension+Async.swift */, + D0D4E4A72C8D921A007F820A /* NetworkExtensionTunnel.swift */, + D0D4E4A82C8D921A007F820A /* NetworkView.swift */, + D0D4E4A92C8D921A007F820A /* OAuth2.swift */, + D0D4E4AA2C8D921A007F820A /* Tunnel.swift */, + D0D4E4AB2C8D921A007F820A /* TunnelButton.swift */, + D0D4E4AC2C8D921A007F820A /* TunnelStatusView.swift */, + D0D4E4A12C8D921A007F820A /* Assets.xcassets */, + D0BF09582C8E6789000D8DEC /* UI.xcconfig */, + ); + path = UI; + sourceTree = ""; + }; + D0D4E5912C8D9D0A007F820A /* Constants */ = { + isa = PBXGroup; + children = ( + D0D4E58E2C8D9D0A007F820A /* Constants.h */, + D0D4E58F2C8D9D0A007F820A /* Constants.swift */, + D0D4E5902C8D9D0A007F820A /* module.modulemap */, + ); + path = Constants; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - D00117372B30341C00D87C25 /* Shared */ = { - isa = PBXNativeTarget; - buildConfigurationList = D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */; - buildPhases = ( - D00117342B30341C00D87C25 /* Sources */, - D00117352B30341C00D87C25 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Shared; - productName = Shared; - productReference = D00117382B30341C00D87C25 /* libBurrowShared.a */; - productType = "com.apple.product-type.library.static"; - }; D020F65229E4A697002790F6 /* NetworkExtension */ = { isa = PBXNativeTarget; buildConfigurationList = D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */; @@ -307,12 +388,12 @@ D0BCC60B2A09A0C100AD070D /* Compile Rust */, D020F64F29E4A697002790F6 /* Sources */, D020F65029E4A697002790F6 /* Frameworks */, - D020F65129E4A697002790F6 /* Resources */, ); buildRules = ( ); dependencies = ( - D00117492B30373500D87C25 /* PBXTargetDependency */, + D0BF09512C8E66F1000D8DEC /* PBXTargetDependency */, + D0D4E5802C8D9C78007F820A /* PBXTargetDependency */, ); name = NetworkExtension; productName = BurrowNetworkExtension; @@ -323,16 +404,18 @@ isa = PBXNativeTarget; buildConfigurationList = D05B9F8129E39EED008CB1F9 /* Build configuration list for PBXNativeTarget "App" */; buildPhases = ( - D04A3E232BAF4AE50043EC85 /* Update Build Number */, D05B9F6E29E39EEC008CB1F9 /* Sources */, D05B9F6F29E39EEC008CB1F9 /* Frameworks */, D05B9F7029E39EEC008CB1F9 /* Resources */, + D0D4E53F2C8D996F007F820A /* Embed Frameworks */, D020F66129E4A697002790F6 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( - D00117472B30373100D87C25 /* PBXTargetDependency */, + D0BF09542C8E66FA000D8DEC /* PBXTargetDependency */, + D0F4FAD22C8DC7960068730A /* PBXTargetDependency */, + D0D4E5882C8D9C88007F820A /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */, ); name = App; @@ -340,6 +423,71 @@ productReference = D05B9F7229E39EEC008CB1F9 /* Burrow.app */; productType = "com.apple.product-type.application"; }; + D0D4E5302C8D996F007F820A /* Core */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E53C2C8D996F007F820A /* Build configuration list for PBXNativeTarget "Core" */; + buildPhases = ( + D0D4E52D2C8D996F007F820A /* Sources */, + D078F7CF2C8DA213008A8CEC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */, + D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */, + D0F759602C8DB24400126CF3 /* PBXTargetDependency */, + ); + name = Core; + packageProductDependencies = ( + D078F7E02C8DA375008A8CEC /* GRPC */, + D078F7E22C8DA375008A8CEC /* SwiftProtobuf */, + D044EE902C8DAB2000778185 /* NIO */, + D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */, + D044EE952C8DAB2800778185 /* NIOTransportServices */, + D0F7597D2C8DB30500126CF3 /* CGRPCZlib */, + ); + productName = Core; + productReference = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; + productType = "com.apple.product-type.framework"; + }; + D0D4E5502C8D9BF2007F820A /* UI */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E5552C8D9BF2007F820A /* Build configuration list for PBXNativeTarget "UI" */; + buildPhases = ( + D0D4E5522C8D9BF2007F820A /* Sources */, + D0D4E5532C8D9BF2007F820A /* Frameworks */, + D0D4E5542C8D9BF2007F820A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D0D4E56F2C8D9C5D007F820A /* PBXTargetDependency */, + ); + name = UI; + packageProductDependencies = ( + ); + productName = Core; + productReference = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */; + productType = "com.apple.product-type.framework"; + }; + D0D4E55A2C8D9BF4007F820A /* Configuration */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D4E55F2C8D9BF4007F820A /* Build configuration list for PBXNativeTarget "Configuration" */; + buildPhases = ( + D0F759912C8DB49E00126CF3 /* Configure Version */, + D0D4E55C2C8D9BF4007F820A /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Configuration; + packageProductDependencies = ( + ); + productName = Core; + productReference = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -347,18 +495,18 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1520; TargetAttributes = { - D00117372B30341C00D87C25 = { - CreatedOnToolsVersion = 15.1; - }; D020F65229E4A697002790F6 = { CreatedOnToolsVersion = 14.3; }; D05B9F7129E39EEC008CB1F9 = { CreatedOnToolsVersion = 14.3; }; + D0D4E5302C8D996F007F820A = { + CreatedOnToolsVersion = 16.0; + }; }; }; buildConfigurationList = D05B9F6D29E39EEC008CB1F9 /* Build configuration list for PBXProject "Burrow" */; @@ -371,6 +519,11 @@ ); mainGroup = D05B9F6929E39EEC008CB1F9; packageReferences = ( + D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, + D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */, + D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */, + D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */, + D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */, ); productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; projectDirPath = ""; @@ -378,52 +531,32 @@ targets = ( D05B9F7129E39EEC008CB1F9 /* App */, D020F65229E4A697002790F6 /* NetworkExtension */, - D00117372B30341C00D87C25 /* Shared */, + D0D4E5502C8D9BF2007F820A /* UI */, + D0D4E5302C8D996F007F820A /* Core */, + D0D4E55A2C8D9BF4007F820A /* Configuration */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - D020F65129E4A697002790F6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D05B9F7029E39EEC008CB1F9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D05B9F7A29E39EED008CB1F9 /* Assets.xcassets in Resources */, D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + D0D4E5542C8D9BF2007F820A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - D04A3E232BAF4AE50043EC85 /* Update Build Number */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(PROJECT_DIR)/../Tools/version.sh", - "$(PROJECT_DIR)/../.git", - ); - name = "Update Build Number"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(PROJECT_DIR)/Configuration/Version.xcconfig", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$PROJECT_DIR/../Tools/version.sh\"\n"; - }; D0BCC60B2A09A0C100AD070D /* Compile Rust */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -444,22 +577,31 @@ shellScript = "\"${PROJECT_DIR}/NetworkExtension/libburrow/build-rust.sh\"\n"; showEnvVarsInLog = 0; }; + D0F759912C8DB49E00126CF3 /* Configure Version */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(PROJECT_DIR)/../Tools/version.sh", + "$(PROJECT_DIR)/../.git", + ); + name = "Configure Version"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(PROJECT_DIR)/Configuration/Version.xcconfig", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$PROJECT_DIR/../Tools/version.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - D00117342B30341C00D87C25 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D001173B2B30341C00D87C25 /* Logging.swift in Sources */, - 0BA6D73C2BA6393200BD4B55 /* NWConnection+Async.swift in Sources */, - D08252762B5C9FC4005DA378 /* Constants.swift in Sources */, - 0BA6D73E2BA6394B00BD4B55 /* DataTypes.swift in Sources */, - 0BA6D73B2BA638D900BD4B55 /* Client.swift in Sources */, - 0BA6D73D2BA6393B00BD4B55 /* NewlineProtocolFramer.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; D020F64F29E4A697002790F6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -472,60 +614,104 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0FAB59A2B818B9600F6A84B /* Network.swift in Sources */, - D0BCC6082A0981FE00AD070D /* Tunnel.swift in Sources */, - D0FAB5982B818B8200F6A84B /* TunnelStatusView.swift in Sources */, - 43AA26D82A10004900F14CE6 /* MenuItemToggleView.swift in Sources */, - D05B9F7829E39EEC008CB1F9 /* BurrowView.swift in Sources */, - D0FAB5922B818A5900F6A84B /* NetworkExtensionTunnel.swift in Sources */, - D000363F2BB895FB00E582EC /* OAuth2.swift in Sources */, - D0FAB5962B818B2900F6A84B /* TunnelButton.swift in Sources */, D00AA8972A4669BC005C8102 /* AppDelegate.swift in Sources */, - D05EF8C82B81818D0017AB4F /* FloatingButtonStyle.swift in Sources */, - D032E6522B8A79C20006B8AD /* HackClub.swift in Sources */, D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */, - D01A79312B81630D0024EC91 /* NetworkView.swift in Sources */, - D032E6542B8A79DA0006B8AD /* WireGuard.swift in Sources */, - D0BCC5FD2A086D4700AD070D /* NetworkExtension+Async.swift in Sources */, - D000363D2BB8928E00E582EC /* NetworkCarouselView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E52D2C8D996F007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */, + D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */, + D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */, + D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E5522C8D9BF2007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5712C8D9C6F007F820A /* HackClub.swift in Sources */, + D0D4E5722C8D9C6F007F820A /* Network.swift in Sources */, + D0D4E5732C8D9C6F007F820A /* WireGuard.swift in Sources */, + D0D4E5742C8D9C6F007F820A /* BurrowView.swift in Sources */, + D0D4E5752C8D9C6F007F820A /* FloatingButtonStyle.swift in Sources */, + D0D4E5762C8D9C6F007F820A /* MenuItemToggleView.swift in Sources */, + D0D4E5772C8D9C6F007F820A /* NetworkCarouselView.swift in Sources */, + D0D4E5782C8D9C6F007F820A /* NetworkExtension+Async.swift in Sources */, + D0D4E5792C8D9C6F007F820A /* NetworkExtensionTunnel.swift in Sources */, + D0D4E57A2C8D9C6F007F820A /* NetworkView.swift in Sources */, + D0D4E57B2C8D9C6F007F820A /* OAuth2.swift in Sources */, + D0D4E57C2C8D9C6F007F820A /* Tunnel.swift in Sources */, + D0D4E57D2C8D9C6F007F820A /* TunnelButton.swift in Sources */, + D0D4E57E2C8D9C6F007F820A /* TunnelStatusView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D4E55C2C8D9BF4007F820A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0D4E5922C8D9D15007F820A /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - D00117472B30373100D87C25 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D00117372B30341C00D87C25 /* Shared */; - targetProxy = D00117462B30373100D87C25 /* PBXContainerItemProxy */; - }; - D00117492B30373500D87C25 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D00117372B30341C00D87C25 /* Shared */; - targetProxy = D00117482B30373500D87C25 /* PBXContainerItemProxy */; - }; D020F65C29E4A697002790F6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D020F65229E4A697002790F6 /* NetworkExtension */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; }; + D0BF09512C8E66F1000D8DEC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E55A2C8D9BF4007F820A /* Configuration */; + targetProxy = D0BF09502C8E66F1000D8DEC /* PBXContainerItemProxy */; + }; + D0BF09542C8E66FA000D8DEC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E55A2C8D9BF4007F820A /* Configuration */; + targetProxy = D0BF09532C8E66FA000D8DEC /* PBXContainerItemProxy */; + }; + D0D4E56F2C8D9C5D007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0D4E56E2C8D9C5D007F820A /* PBXContainerItemProxy */; + }; + D0D4E5802C8D9C78007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0D4E57F2C8D9C78007F820A /* PBXContainerItemProxy */; + }; + D0D4E5882C8D9C88007F820A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5502C8D9BF2007F820A /* UI */; + targetProxy = D0D4E5872C8D9C88007F820A /* PBXContainerItemProxy */; + }; + D0F4FAD22C8DC7960068730A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D4E5302C8D996F007F820A /* Core */; + targetProxy = D0F4FAD12C8DC7960068730A /* PBXContainerItemProxy */; + }; + D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */; + }; + D0F759602C8DB24400126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */; + }; + D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D0F759892C8DB34200126CF3 /* GRPC */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - D001173D2B30341C00D87C25 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; - buildSettings = { - }; - name = Debug; - }; - D001173E2B30341C00D87C25 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D00117422B30348D00D87C25 /* Shared.xcconfig */; - buildSettings = { - }; - name = Release; - }; D020F65F29E4A697002790F6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D020F66229E4A6E5002790F6 /* NetworkExtension.xcconfig */; @@ -568,18 +754,51 @@ }; name = Release; }; + D0D4E53D2C8D996F007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0D4E4F72C8D941D007F820A /* Framework.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E53E2C8D996F007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0D4E4F72C8D941D007F820A /* Framework.xcconfig */; + buildSettings = { + }; + name = Release; + }; + D0D4E5562C8D9BF2007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0BF09582C8E6789000D8DEC /* UI.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E5572C8D9BF2007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0BF09582C8E6789000D8DEC /* UI.xcconfig */; + buildSettings = { + }; + name = Release; + }; + D0D4E5602C8D9BF4007F820A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Configuration.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + D0D4E5612C8D9BF4007F820A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D00117422B30348D00D87C25 /* Configuration.xcconfig */; + buildSettings = { + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - D001173C2B30341C00D87C25 /* Build configuration list for PBXNativeTarget "Shared" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D001173D2B30341C00D87C25 /* Debug */, - D001173E2B30341C00D87C25 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; D020F65E29E4A697002790F6 /* Build configuration list for PBXNativeTarget "NetworkExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -607,7 +826,130 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D0D4E53C2C8D996F007F820A /* Build configuration list for PBXNativeTarget "Core" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E53D2C8D996F007F820A /* Debug */, + D0D4E53E2C8D996F007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D0D4E5552C8D9BF2007F820A /* Build configuration list for PBXNativeTarget "UI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E5562C8D9BF2007F820A /* Debug */, + D0D4E5572C8D9BF2007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D0D4E55F2C8D9BF4007F820A /* Build configuration list for PBXNativeTarget "Configuration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D4E5602C8D9BF4007F820A /* Debug */, + D0D4E5612C8D9BF4007F820A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.72.0; + }; + }; + D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-nio-transport-services.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.21.0; + }; + }; + D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.1; + }; + }; + D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/grpc/grpc-swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.23.0; + }; + }; + D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-protobuf.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.28.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D044EE902C8DAB2000778185 /* NIO */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */; + productName = NIO; + }; + D044EE922C8DAB2000778185 /* NIOConcurrencyHelpers */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */; + productName = NIOConcurrencyHelpers; + }; + D044EE952C8DAB2800778185 /* NIOTransportServices */ = { + isa = XCSwiftPackageProductDependency; + package = D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */; + productName = NIOTransportServices; + }; + D078F7E02C8DA375008A8CEC /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; + D078F7E22C8DA375008A8CEC /* SwiftProtobuf */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = SwiftProtobuf; + }; + D0B1D10F2C436152004B7823 /* AsyncAlgorithms */ = { + isa = XCSwiftPackageProductDependency; + package = D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; + productName = AsyncAlgorithms; + }; + D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = "plugin:GRPCSwiftPlugin"; + }; + D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = "plugin:SwiftProtobufPlugin"; + }; + D0F7597D2C8DB30500126CF3 /* CGRPCZlib */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = CGRPCZlib; + }; + D0F759892C8DB34200126CF3 /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D05B9F6A29E39EEC008CB1F9 /* Project object */; } diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..739b77c --- /dev/null +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "fa512b990383b7e309c5854a5279817052294a8191a6d3c55c49cfb38e88c0c3", + "pins" : [ + { + "identity" : "grpc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift.git", + "state" : { + "revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1", + "version" : "1.23.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "9746cf80e29edfef2a39924a66731249223f42a3", + "version" : "2.72.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "d1ead62745cc3269e482f1c51f27608057174379", + "version" : "1.24.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "b5f7062b60e4add1e8c343ba4eb8da2e324b3a94", + "version" : "1.34.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "7b84abbdcef69cc3be6573ac12440220789dcd69", + "version" : "2.27.2" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", + "version" : "1.28.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5", + "version" : "1.3.2" + } + } + ], + "version" : 3 +} diff --git a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme index 670823d..a524e87 100644 --- a/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme +++ b/Apple/Burrow.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -1,10 +1,11 @@ + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> = { - guard let groupContainerURL = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { - return .failure(.invalidAppGroupIdentifier) - } - return .success(groupContainerURL) - }() public static var socketURL: URL { get throws { try groupContainerURL.appending(component: "burrow.sock", directoryHint: .notDirectory) } } - public static var dbURL: URL { + public static var databaseURL: URL { get throws { try groupContainerURL.appending(component: "burrow.db", directoryHint: .notDirectory) } } + + private static var groupContainerURL: URL { + get throws { try _groupContainerURL.get() } + } + private static let _groupContainerURL: Result = { + switch FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) { + case .some(let url): .success(url) + case .none: .failure(.invalidAppGroupIdentifier) + } + }() +} + +extension Logger { + @_dynamicReplacement(for: subsystem) + public static var subsystem: String { Constants.bundleIdentifier } } diff --git a/Apple/Shared/Constants/module.modulemap b/Apple/Configuration/Constants/module.modulemap similarity index 66% rename from Apple/Shared/Constants/module.modulemap rename to Apple/Configuration/Constants/module.modulemap index 7ee21fc..0e60f32 100644 --- a/Apple/Shared/Constants/module.modulemap +++ b/Apple/Configuration/Constants/module.modulemap @@ -1,4 +1,4 @@ -module Constants { +module CConstants { header "Constants.h" export * } diff --git a/Apple/Configuration/Debug.xcconfig b/Apple/Configuration/Debug.xcconfig new file mode 100644 index 0000000..9529dbd --- /dev/null +++ b/Apple/Configuration/Debug.xcconfig @@ -0,0 +1,26 @@ +// Release +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +SWIFT_COMPILATION_MODE = wholemodule +SWIFT_OPTIMIZATION_LEVEL = -Osize +LLVM_LTO = YES +DEAD_CODE_STRIPPING = YES +STRIP_INSTALLED_PRODUCT = YES +STRIP_SWIFT_SYMBOLS = YES +COPY_PHASE_STRIP = NO +VALIDATE_PRODUCT = YES +ENABLE_MODULE_VERIFIER = YES + +// Debug +ONLY_ACTIVE_ARCH[config=Debug] = YES +DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf +ENABLE_TESTABILITY[config=Debug] = YES +GCC_PREPROCESSOR_DEFINITIONS[config=Debug] = DEBUG=1 $(inherited) +SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG +SWIFT_COMPILATION_MODE[config=Debug] = singlefile +LLVM_LTO[config=Debug] = NO +DEAD_CODE_STRIPPING[config=Debug] = NO +VALIDATE_PRODUCT[config=Debug] = NO +STRIP_INSTALLED_PRODUCT[config=Debug] = NO +STRIP_SWIFT_SYMBOLS[config=Debug] = NO +ENABLE_MODULE_VERIFIER[config=Debug] = NO diff --git a/Apple/Configuration/Extension.xcconfig b/Apple/Configuration/Extension.xcconfig index f8d90a3..5885c31 100644 --- a/Apple/Configuration/Extension.xcconfig +++ b/Apple/Configuration/Extension.xcconfig @@ -1,4 +1,6 @@ -MERGED_BINARY_TYPE = manual +LD_EXPORT_SYMBOLS = NO + +OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -disable-autolink-framework -Xfrontend UIKit -Xfrontend -disable-autolink-framework -Xfrontend AppKit -Xfrontend -disable-autolink-framework -Xfrontend SwiftUI LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks -LD_RUNPATH_SEARCH_PATHS[sdk=macos*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/Apple/Configuration/Framework.xcconfig b/Apple/Configuration/Framework.xcconfig new file mode 100644 index 0000000..6fa4f19 --- /dev/null +++ b/Apple/Configuration/Framework.xcconfig @@ -0,0 +1,14 @@ +PRODUCT_NAME = Burrow$(TARGET_NAME:c99extidentifier) +PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(TARGET_NAME:c99extidentifier) +APPLICATION_EXTENSION_API_ONLY = YES +SWIFT_INSTALL_OBJC_HEADER = NO +SWIFT_SKIP_AUTOLINKING_FRAMEWORKS = YES +SWIFT_SKIP_AUTOLINKING_LIBRARIES = YES + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks +LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks + +DYLIB_INSTALL_NAME_BASE = @rpath +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +VERSIONING_SYSTEM = diff --git a/Apple/Core/Client.swift b/Apple/Core/Client.swift new file mode 100644 index 0000000..8874e3b --- /dev/null +++ b/Apple/Core/Client.swift @@ -0,0 +1,32 @@ +import GRPC +import NIOTransportServices + +public typealias TunnelClient = Burrow_TunnelAsyncClient +public typealias NetworksClient = Burrow_NetworksAsyncClient + +public protocol Client { + init(channel: GRPCChannel) +} + +extension Client { + public static func unix(socketURL: URL) -> Self { + let group = NIOTSEventLoopGroup() + let configuration = ClientConnection.Configuration.default( + target: .unixDomainSocket(socketURL.path), + eventLoopGroup: group + ) + return Self(channel: ClientConnection(configuration: configuration)) + } +} + +extension TunnelClient: Client { + public init(channel: any GRPCChannel) { + self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none) + } +} + +extension NetworksClient: Client { + public init(channel: any GRPCChannel) { + self.init(channel: channel, defaultCallOptions: .init(), interceptors: .none) + } +} diff --git a/Apple/Core/Client/burrow.proto b/Apple/Core/Client/burrow.proto new file mode 120000 index 0000000..03e86a5 --- /dev/null +++ b/Apple/Core/Client/burrow.proto @@ -0,0 +1 @@ +../../../proto/burrow.proto \ No newline at end of file diff --git a/Apple/Core/Client/grpc-swift-config.json b/Apple/Core/Client/grpc-swift-config.json new file mode 100644 index 0000000..2d89698 --- /dev/null +++ b/Apple/Core/Client/grpc-swift-config.json @@ -0,0 +1,11 @@ +{ + "invocations": [ + { + "protoFiles": [ + "burrow.proto", + ], + "server": false, + "visibility": "public" + } + ] +} diff --git a/Apple/Core/Client/swift-protobuf-config.json b/Apple/Core/Client/swift-protobuf-config.json new file mode 100644 index 0000000..87aaec3 --- /dev/null +++ b/Apple/Core/Client/swift-protobuf-config.json @@ -0,0 +1,10 @@ +{ + "invocations": [ + { + "protoFiles": [ + "burrow.proto", + ], + "visibility": "public" + } + ] +} diff --git a/Apple/Shared/Logging.swift b/Apple/Core/Logging.swift similarity index 88% rename from Apple/Shared/Logging.swift rename to Apple/Core/Logging.swift index 36f024c..ba40888 100644 --- a/Apple/Shared/Logging.swift +++ b/Apple/Core/Logging.swift @@ -4,7 +4,7 @@ import os extension Logger { private static let loggers: OSAllocatedUnfairLock<[String: Logger]> = OSAllocatedUnfairLock(initialState: [:]) - public static let subsystem = Constants.bundleIdentifier + public dynamic static var subsystem: String { "com.hackclub.burrow" } public static func logger(for type: Any.Type) -> Logger { let category = String(describing: type) diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 89e0de6..a8e42e0 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,92 +1,74 @@ -import BurrowShared +import AsyncAlgorithms +import BurrowConfiguration +import BurrowCore import libburrow import NetworkExtension import os class PacketTunnelProvider: NEPacketTunnelProvider { + enum Error: Swift.Error { + case missingTunnelConfiguration + } + private let logger = Logger.logger(for: PacketTunnelProvider.self) - private var client: Client? + + private var client: TunnelClient { + get throws { try _client.get() } + } + private let _client: Result = Result { + try TunnelClient.unix(socketURL: Constants.socketURL) + } override init() { do { libburrow.spawnInProcess( socketPath: try Constants.socketURL.path(percentEncoded: false), - dbPath: try Constants.dbURL.path(percentEncoded: false) + databasePath: try Constants.databaseURL.path(percentEncoded: false) ) } catch { - logger.error("Failed to spawn: \(error)") + logger.error("Failed to spawn networking thread: \(error)") } } override func startTunnel(options: [String: NSObject]? = nil) async throws { do { - let client = try Client() - self.client = client - register_events(client) - - _ = try await self.loadTunSettings() - let startRequest = Start( - tun: Start.TunOptions( - name: nil, no_pi: false, tun_excl: false, tun_retrieve: true, address: [] - ) - ) - let response = try await client.request(startRequest, type: BurrowResult.self) - self.logger.log("Received start server response: \(String(describing: response))") + let configuration = try await Array(client.tunnelConfiguration(.init()).prefix(1)).first + guard let settings = configuration?.settings else { + throw Error.missingTunnelConfiguration + } + try await setTunnelNetworkSettings(settings) + _ = try await client.tunnelStart(.init()) + logger.log("Started tunnel with network settings: \(settings)") } catch { - self.logger.error("Failed to start tunnel: \(error)") + logger.error("Failed to start tunnel: \(error)") throw error } } override func stopTunnel(with reason: NEProviderStopReason) async { do { - let client = try Client() - _ = try await client.single_request("Stop", type: BurrowResult.self) - self.logger.log("Stopped client.") + _ = try await client.tunnelStop(.init()) + logger.log("Stopped client") } catch { - self.logger.error("Failed to stop tunnel: \(error)") - } - } - func loadTunSettings() async throws -> ServerConfig { - guard let client = self.client else { - throw BurrowError.noClient - } - let srvConfig = try await client.single_request("ServerConfig", type: BurrowResult.self) - guard let serverconfig = srvConfig.Ok else { - throw BurrowError.resultIsError - } - guard let tunNs = generateTunSettings(from: serverconfig) else { - throw BurrowError.addrDoesntExist - } - try await self.setTunnelNetworkSettings(tunNs) - self.logger.info("Set remote tunnel address to \(tunNs.tunnelRemoteAddress)") - return serverconfig - } - private func generateTunSettings(from: ServerConfig) -> NETunnelNetworkSettings? { - // Using a makeshift remote tunnel address - let nst = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") - var v4Addresses = [String]() - var v6Addresses = [String]() - for addr in from.address { - if IPv4Address(addr) != nil { - v6Addresses.append(addr) - } - if IPv6Address(addr) != nil { - v4Addresses.append(addr) - } - } - nst.ipv4Settings = NEIPv4Settings(addresses: v4Addresses, subnetMasks: v4Addresses.map { _ in - "255.255.255.0" - }) - nst.ipv6Settings = NEIPv6Settings(addresses: v6Addresses, networkPrefixLengths: v6Addresses.map { _ in 64 }) - logger.log("Initialized ipv4 settings: \(nst.ipv4Settings)") - return nst - } - func register_events(_ client: Client) { - client.on_event(.ConfigChange) { (cfig: ServerConfig) in - self.logger.info("Config Change Notification: \(String(describing: cfig))") - self.setTunnelNetworkSettings(self.generateTunSettings(from: cfig)) - self.logger.info("Updated Tunnel Network Settings.") + logger.error("Failed to stop tunnel: \(error)") } } } + +extension Burrow_TunnelConfigurationResponse { + fileprivate var settings: NEPacketTunnelNetworkSettings { + let ipv6Addresses = addresses.filter { IPv6Address($0) != nil } + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") + settings.mtu = NSNumber(value: mtu) + settings.ipv4Settings = NEIPv4Settings( + addresses: addresses.filter { IPv4Address($0) != nil }, + subnetMasks: ["255.255.255.0"] + ) + settings.ipv6Settings = NEIPv6Settings( + addresses: ipv6Addresses, + networkPrefixLengths: ipv6Addresses.map { _ in 64 } + ) + return settings + } +} diff --git a/Apple/NetworkExtension/libburrow/build-rust.sh b/Apple/NetworkExtension/libburrow/build-rust.sh index 00c3652..6f455a9 100755 --- a/Apple/NetworkExtension/libburrow/build-rust.sh +++ b/Apple/NetworkExtension/libburrow/build-rust.sh @@ -68,11 +68,12 @@ else CARGO_PATH="$(dirname $(readlink -f $(which cargo))):/usr/bin" fi -CARGO_PATH="$(dirname $(readlink -f $(which protoc))):$CARGO_PATH" +PROTOC=$(readlink -f $(which protoc)) +CARGO_PATH="$(dirname $PROTOC):$CARGO_PATH" # Run cargo without the various environment variables set by Xcode. # Those variables can confuse cargo and the build scripts it runs. -env -i PATH="$CARGO_PATH" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" +env -i PATH="$CARGO_PATH" PROTOC="$PROTOC" CARGO_TARGET_DIR="${CONFIGURATION_TEMP_DIR}/target" IPHONEOS_DEPLOYMENT_TARGET="$IPHONEOS_DEPLOYMENT_TARGET" MACOSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" cargo build "${CARGO_ARGS[@]}" mkdir -p "${BUILT_PRODUCTS_DIR}" diff --git a/Apple/NetworkExtension/libburrow/libburrow.h b/Apple/NetworkExtension/libburrow/libburrow.h index 2b578ab..59b4734 100644 --- a/Apple/NetworkExtension/libburrow/libburrow.h +++ b/Apple/NetworkExtension/libburrow/libburrow.h @@ -1,2 +1,2 @@ -__attribute__((__swift_name__("spawnInProcess(socketPath:dbPath:)"))) +__attribute__((__swift_name__("spawnInProcess(socketPath:databasePath:)"))) extern void spawn_in_process(const char * __nullable socket_path, const char * __nullable db_path); diff --git a/Apple/Shared/Client.swift b/Apple/Shared/Client.swift deleted file mode 100644 index f643c6c..0000000 --- a/Apple/Shared/Client.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Foundation -import Network - -public final class Client { - let connection: NWConnection - - private let logger = Logger.logger(for: Client.self) - private var generator = SystemRandomNumberGenerator() - private var continuations: [UInt: UnsafeContinuation] = [:] - private var eventMap: [NotificationType: [(Data) throws -> Void]] = [:] - private var task: Task? - - public convenience init() throws { - self.init(url: try Constants.socketURL) - } - - public init(url: URL) { - let endpoint: NWEndpoint - if url.isFileURL { - endpoint = .unix(path: url.path(percentEncoded: false)) - } else { - endpoint = .url(url) - } - - let parameters = NWParameters.tcp - parameters.defaultProtocolStack - .applicationProtocols - .insert(NWProtocolFramer.Options(definition: NewlineProtocolFramer.definition), at: 0) - let connection = NWConnection(to: endpoint, using: parameters) - connection.start(queue: .global()) - self.connection = connection - self.task = Task { [weak self] in - while true { - let (data, _, _) = try await connection.receiveMessage() - let peek = try JSONDecoder().decode(MessagePeek.self, from: data) - switch peek.type { - case .Response: - let response = try JSONDecoder().decode(ResponsePeek.self, from: data) - self?.logger.info("Received response for \(response.id)") - guard let continuations = self?.continuations else {return} - self?.logger.debug("All keys in continuation table: \(continuations.keys)") - guard let continuation = self?.continuations[response.id] else { return } - self?.logger.debug("Got matching continuation") - continuation.resume(returning: data) - case .Notification: - let peek = try JSONDecoder().decode(NotificationPeek.self, from: data) - guard let handlers = self?.eventMap[peek.method] else { continue } - _ = try handlers.map { try $0(data) } - default: - continue - } - } - } - } - private func send(_ request: T) async throws -> U { - let data: Data = try await withUnsafeThrowingContinuation { continuation in - continuations[request.id] = continuation - do { - let data = try JSONEncoder().encode(request) - let completion: NWConnection.SendCompletion = .contentProcessed { error in - guard let error = error else { - return - } - continuation.resume(throwing: error) - } - connection.send(content: data, completion: completion) - } catch { - continuation.resume(throwing: error) - return - } - } - self.logger.debug("Got response data: \(String(describing: data.base64EncodedString()))") - let res = try JSONDecoder().decode(Response.self, from: data) - self.logger.debug("Got response data decoded: \(String(describing: res))") - return res.result - } - public func request(_ request: T, type: U.Type = U.self) async throws -> U { - let req = BurrowRequest( - id: generator.next(upperBound: UInt.max), - command: request - ) - return try await send(req) - } - public func single_request(_ request: String, type: U.Type = U.self) async throws -> U { - let req = BurrowSimpleRequest( - id: generator.next(upperBound: UInt.max), - command: request - ) - return try await send(req) - } - public func on_event(_ event: NotificationType, callable: @escaping (T) throws -> Void) { - let action = { data in - let decoded = try JSONDecoder().decode(Notification.self, from: data) - try callable(decoded.params) - } - if eventMap[event] != nil { - eventMap[event]?.append(action) - } else { - eventMap[event] = [action] - } - } - - deinit { - connection.cancel() - } -} diff --git a/Apple/Shared/DataTypes.swift b/Apple/Shared/DataTypes.swift deleted file mode 100644 index ac49abc..0000000 --- a/Apple/Shared/DataTypes.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Foundation - -// swiftlint:disable identifier_name raw_value_for_camel_cased_codable_enum -public enum BurrowError: Error { - case addrDoesntExist - case resultIsError - case cantParseResult - case resultIsNone - case noClient -} - -public protocol Request: Codable where Params: Codable { - associatedtype Params - - var id: UInt { get set } - var method: String { get set } - var params: Params? { get set } -} - -public enum MessageType: String, Codable { - case Request - case Response - case Notification -} - -public struct MessagePeek: Codable { - public var type: MessageType - public init(type: MessageType) { - self.type = type - } -} - -public struct BurrowSimpleRequest: Request { - public var id: UInt - public var method: String - public var params: String? - public init(id: UInt, command: String, params: String? = nil) { - self.id = id - self.method = command - self.params = params - } -} - -public struct BurrowRequest: Request where T: Codable { - public var id: UInt - public var method: String - public var params: T? - public init(id: UInt, command: T) { - self.id = id - self.method = "\(T.self)" - self.params = command - } -} - -public struct Response: Decodable where T: Decodable { - public var id: UInt - public var result: T - public init(id: UInt, result: T) { - self.id = id - self.result = result - } -} - -public struct ResponsePeek: Codable { - public var id: UInt - public init(id: UInt) { - self.id = id - } -} - -public enum NotificationType: String, Codable { - case ConfigChange -} - -public struct Notification: Codable where T: Codable { - public var method: NotificationType - public var params: T - public init(method: NotificationType, params: T) { - self.method = method - self.params = params - } -} - -public struct NotificationPeek: Codable { - public var method: NotificationType - public init(method: NotificationType) { - self.method = method - } -} - -public struct AnyResponseData: Codable { - public var type: String - public init(type: String) { - self.type = type - } -} - -public struct BurrowResult: Codable where T: Codable { - public var Ok: T? - public var Err: String? - public init(Ok: T, Err: String? = nil) { - self.Ok = Ok - self.Err = Err - } -} - -public struct ServerConfig: Codable { - public let address: [String] - public let name: String? - public let mtu: Int32? - public init(address: [String], name: String?, mtu: Int32?) { - self.address = address - self.name = name - self.mtu = mtu - } -} - -public struct Start: Codable { - public struct TunOptions: Codable { - public let name: String? - public let no_pi: Bool - public let tun_excl: Bool - public let tun_retrieve: Bool - public let address: [String] - public init(name: String?, no_pi: Bool, tun_excl: Bool, tun_retrieve: Bool, address: [String]) { - self.name = name - self.no_pi = no_pi - self.tun_excl = tun_excl - self.tun_retrieve = tun_retrieve - self.address = address - } - } - public let tun: TunOptions - public init(tun: TunOptions) { - self.tun = tun - } -} - -// swiftlint:enable identifier_name raw_value_for_camel_cased_codable_enum diff --git a/Apple/Shared/NWConnection+Async.swift b/Apple/Shared/NWConnection+Async.swift deleted file mode 100644 index c21fdc0..0000000 --- a/Apple/Shared/NWConnection+Async.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Network - -extension NWConnection { - // swiftlint:disable:next large_tuple - func receiveMessage() async throws -> (Data, NWConnection.ContentContext?, Bool) { - try await withUnsafeThrowingContinuation { continuation in - receiveMessage { completeContent, contentContext, isComplete, error in - if let error { - continuation.resume(throwing: error) - } else { - guard let completeContent = completeContent else { - fatalError("Both error and completeContent were nil") - } - continuation.resume(returning: (completeContent, contentContext, isComplete)) - } - } - } - } - - func send(content: Data) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - send(content: content, completion: .contentProcessed { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: ()) - } - }) - } - } -} diff --git a/Apple/Shared/NewlineProtocolFramer.swift b/Apple/Shared/NewlineProtocolFramer.swift deleted file mode 100644 index d2f71e5..0000000 --- a/Apple/Shared/NewlineProtocolFramer.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import Network - -final class NewlineProtocolFramer: NWProtocolFramerImplementation { - private static let delimeter: UInt8 = 10 // `\n` - - static let definition = NWProtocolFramer.Definition(implementation: NewlineProtocolFramer.self) - static let label = "Lines" - - init(framer: NWProtocolFramer.Instance) { } - - func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { .ready } - func stop(framer: NWProtocolFramer.Instance) -> Bool { true } - - func wakeup(framer: NWProtocolFramer.Instance) { } - func cleanup(framer: NWProtocolFramer.Instance) { } - - func handleInput(framer: NWProtocolFramer.Instance) -> Int { - while true { - var result: [Data] = [] - let parsed = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 16_000) { buffer, _ in - guard let buffer else { return 0 } - var lines = buffer - .split(separator: Self.delimeter, omittingEmptySubsequences: false) - .map { Data($0) } - guard lines.count > 1 else { return 0 } - _ = lines.popLast() - - result = lines - return lines.reduce(lines.count) { $0 + $1.count } - } - - guard parsed && !result.isEmpty else { break } - - for line in result { - framer.deliverInput(data: line, message: .init(instance: framer), isComplete: true) - } - } - return 0 - } - - func handleOutput( - framer: NWProtocolFramer.Instance, - message: NWProtocolFramer.Message, - messageLength: Int, - isComplete: Bool - ) { - do { - try framer.writeOutputNoCopy(length: messageLength) - framer.writeOutput(data: [Self.delimeter]) - } catch { - } - } -} diff --git a/Apple/App/Assets.xcassets/AccentColor.colorset/Contents.json b/Apple/UI/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/AccentColor.colorset/Contents.json rename to Apple/UI/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/100.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/100.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/100.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/1024.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/114.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/114.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/114.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/120.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/120.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/128.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/128.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/144.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/144.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/144.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/152.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/152.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/16.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/16.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/167.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/167.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/172.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/172.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/172.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/172.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/180.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/180.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/196.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/196.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/196.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/196.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/20.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/20.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/216.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/216.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/216.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/216.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/256.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/256.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/29.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/29.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/32.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/32.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/40.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/40.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/48.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/48.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/48.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/48.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/50.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/50.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/50.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/512.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/512.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/55.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/55.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/55.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/55.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/57.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/57.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/57.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/58.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/58.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/60.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/60.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/64.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/64.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/72.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/72.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/72.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/76.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/76.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/80.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/80.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/87.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/87.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/88.png b/Apple/UI/Assets.xcassets/AppIcon.appiconset/88.png similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/88.png rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/88.png diff --git a/Apple/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apple/UI/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Apple/UI/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Apple/App/Assets.xcassets/Contents.json b/Apple/UI/Assets.xcassets/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/Contents.json rename to Apple/UI/Assets.xcassets/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.colorset/Contents.json b/Apple/UI/Assets.xcassets/HackClub.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.colorset/Contents.json rename to Apple/UI/Assets.xcassets/HackClub.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/Contents.json b/Apple/UI/Assets.xcassets/HackClub.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.imageset/Contents.json rename to Apple/UI/Assets.xcassets/HackClub.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf b/Apple/UI/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf similarity index 100% rename from Apple/App/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf rename to Apple/UI/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf diff --git a/Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json b/Apple/UI/Assets.xcassets/WireGuard.colorset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.colorset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuard.colorset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json b/Apple/UI/Assets.xcassets/WireGuard.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.imageset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuard.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg b/Apple/UI/Assets.xcassets/WireGuard.imageset/WireGuard.svg similarity index 100% rename from Apple/App/Assets.xcassets/WireGuard.imageset/WireGuard.svg rename to Apple/UI/Assets.xcassets/WireGuard.imageset/WireGuard.svg diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json b/Apple/UI/Assets.xcassets/WireGuardTitle.imageset/Contents.json similarity index 100% rename from Apple/App/Assets.xcassets/WireGuardTitle.imageset/Contents.json rename to Apple/UI/Assets.xcassets/WireGuardTitle.imageset/Contents.json diff --git a/Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg b/Apple/UI/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg similarity index 100% rename from Apple/App/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg rename to Apple/UI/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg diff --git a/Apple/App/BurrowView.swift b/Apple/UI/BurrowView.swift similarity index 95% rename from Apple/App/BurrowView.swift rename to Apple/UI/BurrowView.swift index 3a53762..96467c7 100644 --- a/Apple/App/BurrowView.swift +++ b/Apple/UI/BurrowView.swift @@ -2,11 +2,11 @@ import AuthenticationServices import SwiftUI #if !os(macOS) -struct BurrowView: View { +public struct BurrowView: View { @Environment(\.webAuthenticationSession) private var webAuthenticationSession - var body: some View { + public var body: some View { NavigationStack { VStack { HStack { @@ -35,6 +35,9 @@ struct BurrowView: View { } } + public init() { + } + private func addHackClubNetwork() { Task { try await authenticateWithSlack() diff --git a/Apple/App/FloatingButtonStyle.swift b/Apple/UI/FloatingButtonStyle.swift similarity index 100% rename from Apple/App/FloatingButtonStyle.swift rename to Apple/UI/FloatingButtonStyle.swift diff --git a/Apple/App/MenuItemToggleView.swift b/Apple/UI/MenuItemToggleView.swift similarity index 87% rename from Apple/App/MenuItemToggleView.swift rename to Apple/UI/MenuItemToggleView.swift index 07db51d..ef5e8ee 100644 --- a/Apple/App/MenuItemToggleView.swift +++ b/Apple/UI/MenuItemToggleView.swift @@ -7,11 +7,11 @@ import SwiftUI -struct MenuItemToggleView: View { +public struct MenuItemToggleView: View { @Environment(\.tunnel) var tunnel: Tunnel - var body: some View { + public var body: some View { HStack { VStack(alignment: .leading) { Text("Burrow") @@ -30,10 +30,13 @@ struct MenuItemToggleView: View { .padding(10) .frame(minWidth: 300, minHeight: 32, maxHeight: 32) } + + public init() { + } } extension Tunnel { - fileprivate var toggleDisabled: Bool { + @MainActor fileprivate var toggleDisabled: Bool { switch status { case .disconnected, .permissionRequired, .connected, .disconnecting: false @@ -42,7 +45,7 @@ extension Tunnel { } } - var toggleIsOn: Binding { + @MainActor var toggleIsOn: Binding { Binding { switch status { case .connecting, .reasserting, .connected: diff --git a/Apple/App/NetworkCarouselView.swift b/Apple/UI/NetworkCarouselView.swift similarity index 90% rename from Apple/App/NetworkCarouselView.swift rename to Apple/UI/NetworkCarouselView.swift index b120c60..f969356 100644 --- a/Apple/App/NetworkCarouselView.swift +++ b/Apple/UI/NetworkCarouselView.swift @@ -2,10 +2,10 @@ import SwiftUI struct NetworkCarouselView: View { var networks: [any Network] = [ - HackClub(id: "1"), - HackClub(id: "2"), - WireGuard(id: "4"), - HackClub(id: "5"), + HackClub(id: 1), + HackClub(id: 2), + WireGuard(id: 4), + HackClub(id: 5) ] var body: some View { diff --git a/Apple/App/NetworkExtension+Async.swift b/Apple/UI/NetworkExtension+Async.swift similarity index 82% rename from Apple/App/NetworkExtension+Async.swift rename to Apple/UI/NetworkExtension+Async.swift index 4833efb..5820e7f 100644 --- a/Apple/App/NetworkExtension+Async.swift +++ b/Apple/UI/NetworkExtension+Async.swift @@ -1,6 +1,6 @@ import NetworkExtension -extension NEVPNManager { +extension NEVPNManager: @unchecked @retroactive Sendable { func remove() async throws { _ = try await withUnsafeThrowingContinuation { continuation in removeFromPreferences(completionHandler: completion(continuation)) @@ -14,7 +14,7 @@ extension NEVPNManager { } } -extension NETunnelProviderManager { +extension NETunnelProviderManager: @unchecked @retroactive Sendable { class var managers: [NETunnelProviderManager] { get async throws { try await withUnsafeThrowingContinuation { continuation in @@ -34,7 +34,7 @@ private func completion(_ continuation: UnsafeContinuation) -> (Err } } -private func completion(_ continuation: UnsafeContinuation) -> (T?, Error?) -> Void { +private func completion(_ continuation: UnsafeContinuation) -> (T?, Error?) -> Void { return { value, error in if let error { continuation.resume(throwing: error) diff --git a/Apple/App/NetworkExtensionTunnel.swift b/Apple/UI/NetworkExtensionTunnel.swift similarity index 67% rename from Apple/App/NetworkExtensionTunnel.swift rename to Apple/UI/NetworkExtensionTunnel.swift index 08002de..7aaa3b1 100644 --- a/Apple/App/NetworkExtensionTunnel.swift +++ b/Apple/UI/NetworkExtensionTunnel.swift @@ -1,22 +1,23 @@ -import BurrowShared +import BurrowCore import NetworkExtension @Observable -class NetworkExtensionTunnel: Tunnel { - @MainActor private(set) var status: TunnelStatus = .unknown - private var error: NEVPNError? +public final class NetworkExtensionTunnel: Tunnel { + @MainActor public private(set) var status: TunnelStatus = .unknown + @MainActor private var error: NEVPNError? private let logger = Logger.logger(for: Tunnel.self) private let bundleIdentifier: String - private var tasks: [Task] = [] + private let configurationChanged: Task + private let statusChanged: Task // Each manager corresponds to one entry in the Settings app. // Our goal is to maintain a single manager, so we create one if none exist and delete any extra. - private var managers: [NEVPNManager]? { + @MainActor private var managers: [NEVPNManager]? { didSet { Task { await updateStatus() } } } - private var currentStatus: TunnelStatus { + @MainActor private var currentStatus: TunnelStatus { guard let managers = managers else { guard let error = error else { return .unknown @@ -41,35 +42,40 @@ class NetworkExtensionTunnel: Tunnel { return manager.connection.tunnelStatus } - convenience init() { - self.init(Constants.networkExtensionBundleIdentifier) - } - - init(_ bundleIdentifier: String) { + public init(bundleIdentifier: String) { self.bundleIdentifier = bundleIdentifier let center = NotificationCenter.default - let configurationChanged = Task { [weak self] in - for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) { - await self?.update() + let tunnel: OSAllocatedUnfairLock = .init(initialState: .none) + configurationChanged = Task { + for try await _ in center.notifications(named: .NEVPNConfigurationChange) { + try Task.checkCancellation() + await tunnel.withLock { $0 }?.update() } } - let statusChanged = Task { [weak self] in - for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) { - await self?.updateStatus() + statusChanged = Task { + for try await _ in center.notifications(named: .NEVPNStatusDidChange) { + try Task.checkCancellation() + await tunnel.withLock { $0 }?.updateStatus() } } - tasks = [configurationChanged, statusChanged] + tunnel.withLock { $0 = self } Task { await update() } } private func update() async { do { - managers = try await NETunnelProviderManager.managers + let result = try await NETunnelProviderManager.managers + await MainActor.run { + managers = result + status = currentStatus + } await self.updateStatus() } catch let vpnError as NEVPNError { - error = vpnError + await MainActor.run { + error = vpnError + } } catch { logger.error("Failed to update VPN configurations: \(error)") } @@ -82,12 +88,7 @@ class NetworkExtensionTunnel: Tunnel { } func configure() async throws { - if managers == nil { - await update() - } - - guard let managers = managers else { return } - + let managers = try await NETunnelProviderManager.managers if managers.count > 1 { try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in for manager in managers.suffix(from: 1) { @@ -110,9 +111,9 @@ class NetworkExtensionTunnel: Tunnel { try await manager.save() } - func start() { - guard let manager = managers?.first else { return } + public func start() { Task { + guard let manager = try await NETunnelProviderManager.managers.first else { return } do { if !manager.isEnabled { manager.isEnabled = true @@ -125,12 +126,14 @@ class NetworkExtensionTunnel: Tunnel { } } - func stop() { - guard let manager = managers?.first else { return } - manager.connection.stopVPNTunnel() + public func stop() { + Task { + guard let manager = try await NETunnelProviderManager.managers.first else { return } + manager.connection.stopVPNTunnel() + } } - func enable() { + public func enable() { Task { do { try await configure() @@ -141,7 +144,8 @@ class NetworkExtensionTunnel: Tunnel { } deinit { - tasks.forEach { $0.cancel() } + configurationChanged.cancel() + statusChanged.cancel() } } diff --git a/Apple/App/NetworkView.swift b/Apple/UI/NetworkView.swift similarity index 100% rename from Apple/App/NetworkView.swift rename to Apple/UI/NetworkView.swift diff --git a/Apple/App/Networks/HackClub.swift b/Apple/UI/Networks/HackClub.swift similarity index 76% rename from Apple/App/Networks/HackClub.swift rename to Apple/UI/Networks/HackClub.swift index f7df674..b1c2023 100644 --- a/Apple/App/Networks/HackClub.swift +++ b/Apple/UI/Networks/HackClub.swift @@ -1,10 +1,14 @@ +import BurrowCore import SwiftUI struct HackClub: Network { - var id: String + typealias NetworkType = Burrow_WireGuardNetwork + static let type: Burrow_NetworkType = .hackClub + + var id: Int32 var backgroundColor: Color { .init("HackClub") } - var label: some View { + @MainActor var label: some View { GeometryReader { reader in VStack(alignment: .leading) { Image("HackClub") diff --git a/Apple/UI/Networks/Network.swift b/Apple/UI/Networks/Network.swift new file mode 100644 index 0000000..c6d5fba --- /dev/null +++ b/Apple/UI/Networks/Network.swift @@ -0,0 +1,36 @@ +import Atomics +import BurrowCore +import SwiftProtobuf +import SwiftUI + +protocol Network { + associatedtype NetworkType: Message + associatedtype Label: View + + static var type: Burrow_NetworkType { get } + + var id: Int32 { get } + var backgroundColor: Color { get } + + @MainActor var label: Label { get } +} + +@Observable +@MainActor +final class NetworkViewModel: Sendable { + private(set) var networks: [Burrow_Network] = [] + + private var task: Task! + + init(socketURL: URL) { + task = Task { [weak self] in + let client = NetworksClient.unix(socketURL: socketURL) + for try await networks in client.networkList(.init()) { + guard let viewModel = self else { continue } + Task { @MainActor in + viewModel.networks = networks.network + } + } + } + } +} diff --git a/Apple/App/Networks/WireGuard.swift b/Apple/UI/Networks/WireGuard.swift similarity index 82% rename from Apple/App/Networks/WireGuard.swift rename to Apple/UI/Networks/WireGuard.swift index 499288a..cba67ef 100644 --- a/Apple/App/Networks/WireGuard.swift +++ b/Apple/UI/Networks/WireGuard.swift @@ -1,10 +1,14 @@ +import BurrowCore import SwiftUI struct WireGuard: Network { - var id: String + typealias NetworkType = Burrow_WireGuardNetwork + static let type: BurrowCore.Burrow_NetworkType = .wireGuard + + var id: Int32 var backgroundColor: Color { .init("WireGuard") } - var label: some View { + @MainActor var label: some View { GeometryReader { reader in VStack(alignment: .leading) { HStack { diff --git a/Apple/App/OAuth2.swift b/Apple/UI/OAuth2.swift similarity index 94% rename from Apple/App/OAuth2.swift rename to Apple/UI/OAuth2.swift index 9a930c9..0fafc8d 100644 --- a/Apple/App/OAuth2.swift +++ b/Apple/UI/OAuth2.swift @@ -1,5 +1,6 @@ import AuthenticationServices import Foundation +import os import SwiftUI enum OAuth2 { @@ -25,11 +26,16 @@ enum OAuth2 { var clientID: String var clientSecret: String - fileprivate static var queue: [Int: CheckedContinuation] = [:] + fileprivate static let queue: OSAllocatedUnfairLock<[Int: CheckedContinuation]> = { + .init(initialState: [:]) + }() fileprivate static func handle(url: URL) { - let continuations = queue - queue.removeAll() + let continuations = queue.withLock { continuations in + let copy = continuations + continuations.removeAll() + return copy + } for (_, continuation) in continuations { continuation.resume(returning: url) } @@ -56,7 +62,7 @@ enum OAuth2 { var queryItems: [URLQueryItem] = [ .init(name: "client_id", value: clientID), .init(name: "response_type", value: responseType.rawValue), - .init(name: "redirect_uri", value: redirectURI.absoluteString), + .init(name: "redirect_uri", value: redirectURI.absoluteString) ] if !scopes.isEmpty { queryItems.append(.init(name: "scope", value: scopes.joined(separator: ","))) @@ -206,6 +212,9 @@ enum OAuth2 { } } +extension WebAuthenticationSession: @unchecked @retroactive Sendable { +} + extension WebAuthenticationSession { #if canImport(BrowserEngineKit) @available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) @@ -243,12 +252,12 @@ extension WebAuthenticationSession { let id = Int.random(in: 0..