From 0a83b8ddaf527975eb4a6746e8dab2d7b3bb983e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Jun 2024 18:11:24 +0200 Subject: [PATCH] [PM-7846] Implement a rust based native messaging proxy and IPC system --- .github/workflows/build-desktop.yml | 24 +- apps/desktop/desktop_native/Cargo.lock | 334 ++++++++++++++++-- apps/desktop/desktop_native/Cargo.toml | 2 +- apps/desktop/desktop_native/build.js | 56 +++ apps/desktop/desktop_native/core/Cargo.toml | 6 + .../desktop_native/core/src/ipc/client.rs | 91 +++++ .../desktop_native/core/src/ipc/mod.rs | 27 ++ .../desktop_native/core/src/ipc/server.rs | 190 ++++++++++ apps/desktop/desktop_native/core/src/lib.rs | 1 + apps/desktop/desktop_native/napi/Cargo.toml | 6 +- apps/desktop/desktop_native/napi/build.js | 24 -- apps/desktop/desktop_native/napi/index.d.ts | 20 ++ apps/desktop/desktop_native/napi/index.js | 3 +- apps/desktop/desktop_native/napi/package.json | 1 - apps/desktop/desktop_native/napi/src/lib.rs | 86 +++++ apps/desktop/desktop_native/proxy/Cargo.toml | 16 + apps/desktop/desktop_native/proxy/src/main.rs | 102 ++++++ apps/desktop/electron-builder.json | 4 +- apps/desktop/package.json | 2 +- apps/desktop/resources/native-messaging.bat | 7 - apps/desktop/src/entry.ts | 37 +- apps/desktop/src/main.ts | 1 + .../desktop/src/main/native-messaging.main.ts | 83 ++--- apps/desktop/src/proxy/ipc.ts | 78 ---- .../src/proxy/native-messaging-proxy.ts | 23 -- apps/desktop/src/proxy/nativemessage.ts | 95 ----- 26 files changed, 961 insertions(+), 358 deletions(-) create mode 100644 apps/desktop/desktop_native/build.js create mode 100644 apps/desktop/desktop_native/core/src/ipc/client.rs create mode 100644 apps/desktop/desktop_native/core/src/ipc/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/ipc/server.rs delete mode 100644 apps/desktop/desktop_native/napi/build.js create mode 100644 apps/desktop/desktop_native/proxy/Cargo.toml create mode 100644 apps/desktop/desktop_native/proxy/src/main.rs delete mode 100644 apps/desktop/resources/native-messaging.bat delete mode 100644 apps/desktop/src/proxy/ipc.ts delete mode 100644 apps/desktop/src/proxy/native-messaging-proxy.ts delete mode 100644 apps/desktop/src/proxy/nativemessage.ts diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 9e59bc47853..fae724299b0 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -180,14 +180,14 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi + working-directory: apps/desktop/desktop_native env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - npm run build:cross-platform + node build.js cross-platform - name: Build application run: npm run dist:lin @@ -306,8 +306,8 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build & Sign (dev) env: @@ -589,8 +589,8 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build application (dev) run: npm run build @@ -753,8 +753,8 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -962,8 +962,8 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1157,8 +1157,8 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0b6dc631049..ba26c645b00 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -88,9 +88,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -119,6 +119,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + [[package]] name = "cbc" version = "0.1.2" @@ -130,9 +136,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.99" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" [[package]] name = "cfg-expr" @@ -283,6 +289,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.6.0" @@ -304,9 +319,13 @@ dependencies = [ "base64", "cbc", "core-foundation", + "dirs", + "futures", "gio", + "interprocess", "keytar", "libsecret", + "log", "rand", "retry", "scopeguard", @@ -314,6 +333,8 @@ dependencies = [ "security-framework-sys", "sha2", "thiserror", + "tokio", + "tokio-util", "typenum", "widestring", "windows", @@ -328,6 +349,21 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "tokio", + "tokio-util", +] + +[[package]] +name = "desktop_proxy" +version = "0.0.0" +dependencies = [ + "anyhow", + "desktop_core", + "futures", + "log", + "simplelog", + "tokio", + "tokio-util", ] [[package]] @@ -340,6 +376,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dlib" version = "0.5.2" @@ -349,6 +406,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -368,7 +431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -395,6 +458,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -402,6 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -438,6 +517,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "futures-task" version = "0.3.30" @@ -450,9 +535,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -523,14 +612,14 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "glib" -version = "0.19.8" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b664491bc77ab55daa6714a592cdbe1a55e28abec09cb50e87689b90de456ff4" +checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" dependencies = [ "bitflags", "futures-channel", @@ -550,9 +639,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.8" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d405205a405182f95e637710850a8e82f25ba01fdd6baebc82dabeaf0883376" +checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7" dependencies = [ "heck", "proc-macro-crate", @@ -606,7 +695,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -629,6 +718,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "keytar" version = "0.1.6" @@ -658,14 +768,24 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.5", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libsecret" version = "0.5.0" @@ -719,9 +839,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -745,10 +865,21 @@ dependencies = [ ] [[package]] -name = "napi" -version = "2.16.6" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "napi" +version = "2.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633e41b2b983cf7983134f0c50986ca524d0caf38a2c6fc893ea3fa2e26abb0c" dependencies = [ "bitflags", "ctor", @@ -766,9 +897,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.5" +version = "2.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" +checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8" dependencies = [ "cfg-if", "convert_case", @@ -780,9 +911,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.67" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff2c00437f3b3266391eb5e6aa25d0029187daf5caf05b8e3271468fb5ae73e" +checksum = "b370b784440c65eb9001d839012eb912ee43e3a2d0361e2c30c13052372c39fe" dependencies = [ "convert_case", "once_cell", @@ -824,6 +955,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -834,6 +971,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -935,9 +1081,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -948,6 +1094,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_pipe" version = "1.2.0" @@ -955,7 +1107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1009,6 +1161,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1026,9 +1184,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1081,6 +1239,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.2" @@ -1090,6 +1254,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -1144,7 +1319,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1234,6 +1409,17 @@ dependencies = [ "digest", ] +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1250,10 +1436,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "syn" -version = "2.0.66" +name = "socket2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -1288,7 +1484,7 @@ dependencies = [ "cfg-if", "fastrand", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1320,6 +1516,39 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.38.0" @@ -1327,8 +1556,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", + "bytes", + "libc", + "mio", "num_cpus", "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -1517,7 +1776,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1573,6 +1832,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index c6b77473b2a..6525b38162d 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["napi", "core"] +members = ["napi", "core", "proxy"] diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js new file mode 100644 index 00000000000..630c3e1e842 --- /dev/null +++ b/apps/desktop/desktop_native/build.js @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const process = require("process"); + +let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; + +function buildNapiModule(target) { + const targetArg = target ? `--target ${target}` : ""; + return child_process.execSync(`npm run build --release -- ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); +} + +function buildProxyBin(target) { + const targetArg = target ? `--target ${target}` : ""; + return child_process.execSync(`cargo build --release ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); +} + +if (!crossPlatform) { + buildNapiModule(); + buildProxyBin(); + return; +} + +let targets = []; +switch (process.platform) { + case "win32": + targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]; + break; + + case "darwin": + targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"]; + break; + + default: + targets = ['x86_64-unknown-linux-musl']; + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; + break; +} + +targets.forEach(target => { + buildNapiModule(target); + buildProxyBin(target); +}); + +if (process.platform === "darwin") { + fs.mkdirSync(path.join(__dirname, "target", "darwin-universal"), { recursive: true }); + + let command = `lipo -create -output ${path.join(__dirname, "target", "darwin-universal", "desktop_proxy")} `; + targets.forEach(target => { + command += `${path.join(__dirname, "target", target, "release", "desktop_proxy")} `; + }); + child_process.execSync(command, { stdio: 'inherit', cwd: __dirname}); + +} diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 1beb2ee08ce..c1c090cb93b 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -17,11 +17,17 @@ arboard = { version = "=3.4.0", default-features = false, features = [ ] } base64 = "=0.22.1" cbc = { version = "=0.1.2", features = ["alloc"] } +dirs = "5.0.1" +futures = "0.3.30" +interprocess = { version = "2.2.0", features = ["tokio"] } +log = "0.4.21" rand = "=0.8.5" retry = "=2.0.0" scopeguard = "=1.2.0" sha2 = "=0.10.8" thiserror = "=1.0.61" +tokio = { version = "1.38.0", features = ["io-util", "sync", "macros"] } +tokio-util = "0.7.11" typenum = "=1.17.0" [target.'cfg(windows)'.dependencies] diff --git a/apps/desktop/desktop_native/core/src/ipc/client.rs b/apps/desktop/desktop_native/core/src/ipc/client.rs new file mode 100644 index 00000000000..042ffee0e46 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/client.rs @@ -0,0 +1,91 @@ +use std::time::Duration; + +use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, ToFsName, +}; +use log::{error, info}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + time::sleep, +}; + +pub async fn connect( + tx: tokio::sync::mpsc::Sender, + mut rx: tokio::sync::mpsc::Receiver, +) { + // Keep track of connection failures to make sure we don't leave the process as a zombie + let mut connection_failures = 0; + + loop { + match connect_inner(&tx, &mut rx).await { + Ok(()) => return, + Err(e) => { + connection_failures += 1; + if connection_failures >= 20 { + error!("Failed to connect to IPC server after 20 attempts: {e}"); + return; + } + + error!("Failed to connect to IPC server: {e}"); + } + } + + sleep(Duration::from_secs(5)).await; + } +} + +async fn connect_inner( + tx: &tokio::sync::mpsc::Sender, + rx: &mut tokio::sync::mpsc::Receiver, +) -> Result<(), Box> { + let path = super::path("bitwarden"); + + info!("Attempting to connect to {}", path.display()); + + let name = path.as_os_str().to_fs_name::()?; + let mut conn = Stream::connect(name).await?; + + info!("Connected to {}", path.display()); + + tx.send("{\"command\":\"connected\"}".to_owned()).await?; + + let mut buffer = vec![0; 8192]; + + // Listen to IPC messages + loop { + tokio::select! { + // Send messages to the IPC server + msg = rx.recv() => { + match msg { + Some(msg) => { + conn.write_all(msg.as_bytes()).await?; + } + None => break, + } + }, + + // Read messages from the IPC server + res = conn.read(&mut buffer[..]) => { + match res { + Err(e) => { + error!("Error reading from IPC server: {e}"); + tx.send("{\"command\":\"disconnected\"}".to_owned()).await?; + break; + } + Ok(0) => { + info!("Connection closed"); + tx.send("{\"command\":\"disconnected\"}".to_owned()).await?; + break; + } + Ok(n) => { + let message = String::from_utf8_lossy(&buffer[..n]).to_string(); + tx.send(message).await?; + } + } + } + } + } + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs new file mode 100644 index 00000000000..ecf60f129ba --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -0,0 +1,27 @@ +pub mod client; +pub mod server; + +// TODO: We probably can find a better location for the IPC socket on Mac. +// One idea is to use App Groups if we have the entitlement: +// https://developer.apple.com/documentation/xcode/configuring-app-groups +// Then we can write the socket to /Users//Library/Group Containers/group.com.bitwarden./ipc.sock +// We might also be able to write to the Apps Container directory from the proxy binary: +// /Users//Library/Containers/com.bitwarden.desktop/Data + +/// Resolve the path to the IPC socket. +pub fn path(name: &str) -> std::path::PathBuf { + let home = dirs::home_dir().unwrap(); + + if cfg!(windows) { + // Use a unique IPC pipe //./pipe/bitwarden.xxxxxxxxxxxxxxxxx.sock per user. + // Hashing prevents problems with reserved characters and file length limitations. + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + use sha2::Digest; + let hash = sha2::Sha256::digest(home.as_os_str().as_encoded_bytes()); + let hash_b64 = URL_SAFE_NO_PAD.encode(hash.as_slice()); + + format!(r"\\.\pipe\{hash_b64}app.{name}").into() + } else { + home.join("tmp").join(format!("app.{name}")) + } +} diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs new file mode 100644 index 00000000000..35a1aa84c8a --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -0,0 +1,190 @@ +use std::error::Error; + +use futures::{Sink, SinkExt, TryFutureExt}; + +use anyhow::Result; +use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions}; +use log::{error, info}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + sync::broadcast::Receiver, +}; +use tokio_util::sync::CancellationToken; + +#[derive(Debug)] +pub struct Message { + pub client_id: u32, + pub kind: MessageType, + pub message: String, +} + +#[derive(Debug)] +pub enum MessageType { + Connected, + Disconnected, + Message, +} + +pub struct Server { + cancel_token: CancellationToken, + tx_connections: tokio::sync::broadcast::Sender, +} + +impl Server { + pub fn start(name: &str, tx: T) -> Result> + where + T: Sink + Unpin + Send + Clone + 'static, + >::Error: std::error::Error + 'static, + { + let path = super::path(name); + + // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. + // Any processes that were using the old socket will remain connected but any new connections will use the new socket. + if !cfg!(windows) { + let _ = std::fs::remove_file(&path); + } + + let name = path.as_os_str().to_fs_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = opts.create_tokio()?; + + let (tx_connections, rx_connections) = tokio::sync::broadcast::channel::(32); + + let cancel_token = CancellationToken::new(); + let cancel_token2 = cancel_token.clone(); + + tokio::spawn(async move { + futures::pin_mut!(listener); + let mut next_client_id = 1_u32; + + loop { + tokio::select! { + _ = cancel_token2.cancelled() => { + info!("IPC server cancelled."); + break; + }, + + msg = listener.accept() => { + match msg { + Ok(stream) => { + let client_id = next_client_id; + next_client_id += 1; + + let rx_connections = rx_connections.resubscribe(); + let tx = tx.clone(); + let cancel_token_clone = cancel_token2.clone(); + + tokio::spawn(handle_connection(stream, tx, rx_connections, cancel_token_clone, client_id).map_err(|e| { + error!("Error handling connection: {}", e) + })); + }, + Err(e) => { + error!("Error reading message: {}", e); + break; + }, + } + } + } + } + }); + + Ok(Self { + cancel_token, + tx_connections, + }) + } + + pub fn send(&self, message: String) -> Result<()> { + self.tx_connections.send(message)?; + Ok(()) + } + + pub fn stop(&self) { + self.cancel_token.cancel(); + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.stop(); + } +} + +async fn handle_connection( + mut stream: impl AsyncRead + AsyncWrite + Unpin, + mut tx: T, + mut rx_connections: Receiver, + cancel_token: CancellationToken, + client_id: u32, +) -> Result<(), Box> +where + T: Sink + Unpin, + >::Error: std::error::Error + 'static, +{ + tx.send(Message { + client_id, + kind: MessageType::Connected, + message: "Connected".to_owned(), + }) + .await?; + + let mut buf = vec![0u8; 8192]; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Client {client_id} cancelled."); + break; + }, + + msg = rx_connections.recv() => { + match msg { + Ok(msg) => { + stream.write_all(msg.as_bytes()).await?; + }, + Err(e) => { + info!("Error reading message: {}", e); + break; + } + } + }, + + result = stream.read(&mut buf) => { + match result { + Err(e) => { + info!("Error reading from client {client_id}: {e}"); + + tx.send(Message { + client_id, + kind: MessageType::Disconnected, + message: "Disconnected".to_owned(), + }).await?; + break; + }, + Ok(0) => { + info!("Client {client_id} disconnected."); + + tx.send(Message { + client_id, + kind: MessageType::Disconnected, + message: "Disconnected".to_owned(), + }).await?; + break; + }, + Ok(size) => { + let msg = std::str::from_utf8(&buf[..size])?; + + tx.send(Message { + client_id, + kind: MessageType::Message, + message: msg.to_string(), + }).await?; + }, + + } + } + } + } + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index 45933060270..2b54b580709 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -2,4 +2,5 @@ pub mod biometric; pub mod clipboard; pub mod crypto; pub mod error; +pub mod ipc; pub mod password; diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 942ccdba212..6fb710b0671 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -16,8 +16,10 @@ manual_test = [] [dependencies] anyhow = "=1.0.86" desktop_core = { path = "../core" } -napi = { version = "=2.16.6", features = ["async"] } -napi-derive = "=2.16.5" +napi = { version = "=2.16.7", features = ["async"] } +napi-derive = "=2.16.6" +tokio = { version = "1.38.0" } +tokio-util = "0.7.11" [build-dependencies] napi-build = "=2.1.3" diff --git a/apps/desktop/desktop_native/napi/build.js b/apps/desktop/desktop_native/napi/build.js deleted file mode 100644 index 6c92dbad1b6..00000000000 --- a/apps/desktop/desktop_native/napi/build.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const child_process = require("child_process"); -const process = require("process"); - -let targets = []; -switch (process.platform) { - case "win32": - targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]; - break; - - case "darwin": - targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"]; - break; - - default: - targets = ['x86_64-unknown-linux-musl']; - process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; - process.env["PKG_CONFIG_ALL_STATIC"] = "1"; - break; -} - -targets.forEach(target => { - child_process.execSync(`npm run build -- --target ${target}`, {stdio: 'inherit'}); -}); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index abfc998de04..39156b8be21 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -41,3 +41,23 @@ export namespace clipboards { export function read(): Promise export function write(text: string, password: boolean): Promise } +export namespace ipc { + export const enum IpcMessageType { + Connected = 0, + Disconnected = 1, + Message = 2 + } + export class IpcMessage { + clientId: number + kind: IpcMessageType + message: string + } + export class IpcServer { + /** Create and start the IPC server. */ + static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): IpcServer + /** Stop the IPC server. */ + stop(): void + /** Send a message over the IPC server. */ + send(message: string): void + } +} diff --git a/apps/desktop/desktop_native/napi/index.js b/apps/desktop/desktop_native/napi/index.js index 75617ee2f1a..87b11e755bf 100644 --- a/apps/desktop/desktop_native/napi/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -206,8 +206,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { passwords, biometrics, clipboards } = nativeBinding +const { passwords, biometrics, clipboards, ipc } = nativeBinding module.exports.passwords = passwords module.exports.biometrics = biometrics module.exports.clipboards = clipboards +module.exports.ipc = ipc diff --git a/apps/desktop/desktop_native/napi/package.json b/apps/desktop/desktop_native/napi/package.json index 70e472b3952..09d1eb5970d 100644 --- a/apps/desktop/desktop_native/napi/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -5,7 +5,6 @@ "scripts": { "build": "napi build --release --platform --js false", "build:debug": "napi build --platform --js false", - "build:cross-platform": "node build.js", "test": "cargo test" }, "author": "", diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index e2e7eb7244f..80137dc5375 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -142,3 +142,89 @@ pub mod clipboards { .map_err(|e| napi::Error::from_reason(e.to_string())) } } + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use tokio_util::sync::PollSender; + + #[napi] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: String, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct IpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl IpcServer { + /// Create and start the IPC server. + #[napi(factory)] + pub fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (tx, mut rx) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = rx.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let server = desktop_core::ipc::server::Server::start(&name, PollSender::new(tx)) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + + Ok(IpcServer { server }) + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server. + #[napi] + pub fn send(&self, message: String) -> napi::Result<()> { + self.server + .send(message) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + } +} diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml new file mode 100644 index 00000000000..0d80ccc925c --- /dev/null +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -0,0 +1,16 @@ +[package] +edition = "2021" +exclude = ["index.node"] +license = "GPL-3.0" +name = "desktop_proxy" +version = "0.0.0" +publish = false + +[dependencies] +anyhow = "=1.0.86" +desktop_core = { path = "../core" } +futures = "0.3.30" +log = "0.4.21" +simplelog = "0.12.2" +tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] } +tokio-util = { version = "0.7.11", features = ["codec"] } diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs new file mode 100644 index 00000000000..cb1976412b2 --- /dev/null +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -0,0 +1,102 @@ +use std::fs::File; + +use futures::{SinkExt, StreamExt}; +use log::*; +use tokio_util::codec::LengthDelimitedCodec; + +fn init_logging() { + use simplelog::{ColorChoice, CombinedLogger, Config, TermLogger, TerminalMode, WriteLogger}; + + let exe = std::env::current_exe().unwrap(); + let parent = exe.parent().unwrap(); + + let level = LevelFilter::Info; + let config = Config::default(); + let log_file = File::create(parent.join("proxy.log")).expect("Can't create file"); + + CombinedLogger::init(vec![ + WriteLogger::new(level, config.clone(), log_file), + TermLogger::new(level, config, TerminalMode::Stderr, ColorChoice::Auto), + ]) + .unwrap(); +} + +/// Bitwarden IPC Proxy. +/// +/// This proxy allows browser extensions to communicate with a desktop application using Native +/// Messaging. This method allows an extension to send and receive messages through the use of +/// stdin/stdout streams. +/// +/// However, this also requires the browser to start the process in order for the communication to +/// occur. To overcome this limitation, we implement Inter-Process Communication (IPC) to establish +/// a stable communication channel between the proxy and the running desktop application. +/// +/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop +/// +#[tokio::main(flavor = "current_thread")] +async fn main() { + init_logging(); + info!("Starting Bitwarden IPC Proxy."); + + // Setup two channels, one for sending messages to the desktop application and one for receiving messages + let (in_tx, in_rx) = tokio::sync::mpsc::channel(32); + let (out_tx, mut out_rx) = tokio::sync::mpsc::channel(32); + + let mut handle = tokio::spawn(desktop_core::ipc::client::connect(out_tx, in_rx)); + + // Create a new codec for reading and writing messages from stdin/stdout. + let mut stdin = LengthDelimitedCodec::builder() + .max_frame_length(8192) + .native_endian() + .new_read(tokio::io::stdin()); + let mut stdout = LengthDelimitedCodec::builder() + .max_frame_length(8192) + .native_endian() + .new_write(tokio::io::stdout()); + + loop { + tokio::select! { + // IPC client has finished, so we should exit as well. + _ = &mut handle => { + break; + } + + // Receive messages from IPC and print to STDOUT. + msg = out_rx.recv() => { + match msg { + Some(msg) => { + debug!("OUT: {}", msg); + stdout.send(msg.into()).await.unwrap(); + } + None => { + // Channel closed, exit. + break; + } + } + }, + + // Listen to stdin and send messages to ipc processor. + msg = stdin.next() => { + match msg { + Some(Ok(msg)) => { + let m = String::from_utf8(msg.to_vec()).unwrap(); + debug!("IN: {}", m); + in_tx.send(m).await.unwrap(); + } + Some(Err(e)) => { + // Unexpected error, exit. + error!("Error parsing input: {}", e); + break; + } + None => { + // EOF, exit. + break; + } + } + } + + } + } + + info!("Exiting."); +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index a0dde225233..aff5e993d71 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -85,8 +85,8 @@ "filter": ["**/*"] }, { - "from": "resources/native-messaging.bat", - "to": "native-messaging.bat" + "from": "desktop_native/target/release/desktop_proxy", + "to": "desktop_proxy" } ] }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f1639dc51a1..7fe42f8b3ba 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,7 +18,7 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", - "build-native": "cd desktop_native/napi && npm run build", + "build-native": "cd desktop_native && node build.js", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", diff --git a/apps/desktop/resources/native-messaging.bat b/apps/desktop/resources/native-messaging.bat deleted file mode 100644 index 45519250dd6..00000000000 --- a/apps/desktop/resources/native-messaging.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -:: Helper script for starting the Native Messaging Proxy on Windows. - -cd ../ -set ELECTRON_RUN_AS_NODE=1 -set ELECTRON_NO_ATTACH_CONSOLE=1 -Bitwarden.exe resources/app.asar %* diff --git a/apps/desktop/src/entry.ts b/apps/desktop/src/entry.ts index 78fe51e8b9e..463ccdd14ce 100644 --- a/apps/desktop/src/entry.ts +++ b/apps/desktop/src/entry.ts @@ -1,35 +1,4 @@ -import { NativeMessagingProxy } from "./proxy/native-messaging-proxy"; +import { Main } from "./main"; -// We need to import the other dependencies using `require` since `import` will -// generate `Error: Cannot find module 'electron'`. The cause of this error is -// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows -// which removes the electron module. This flag is needed for stdin/out to work -// properly on Windows. - -if ( - process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1) -) { - if (process.platform === "darwin") { - // eslint-disable-next-line - const app = require("electron").app; - - app.on("ready", () => { - app.dock.hide(); - }); - } - - process.stdout.on("error", (e) => { - if (e.code === "EPIPE") { - process.exit(0); - } - }); - - const proxy = new NativeMessagingProxy(); - proxy.run(); -} else { - // eslint-disable-next-line - const Main = require("./main").Main; - - const main = new Main(); - main.bootstrap(); -} +const main = new Main(); +main.bootstrap(); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f2295f2cdd8..ef4f244b015 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -214,6 +214,7 @@ export class Main { this.windowMain, app.getPath("userData"), app.getPath("exe"), + app.getAppPath(), ); this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 8c8404578b6..14cf33880a8 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -1,27 +1,27 @@ import { existsSync, promises as fs } from "fs"; -import { Socket } from "net"; import { homedir, userInfo } from "os"; import * as path from "path"; import * as util from "util"; import { ipcMain } from "electron"; -import * as ipc from "node-ipc"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ipc } from "@bitwarden/desktop-napi"; -import { getIpcSocketRoot } from "../proxy/ipc"; +import { isDev } from "../utils"; import { WindowMain } from "./window.main"; export class NativeMessagingMain { - private connected: Socket[] = []; - private socket: any; + private ipcServer: ipc.IpcServer | null; + private connected: number[] = []; constructor( private logService: LogService, private windowMain: WindowMain, private userPath: string, private exePath: string, + private appPath: string, ) { ipcMain.handle( "nativeMessaging.manifests", @@ -73,55 +73,46 @@ export class NativeMessagingMain { } listen() { - ipc.config.id = "bitwarden"; - ipc.config.retry = 1500; - const ipcSocketRoot = getIpcSocketRoot(); - if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; + if (this.ipcServer) { + this.ipcServer.stop(); } - ipc.serve(() => { - ipc.server.on("message", (data: any, socket: any) => { - this.socket = socket; - this.windowMain.win.webContents.send("nativeMessaging", data); - }); + this.ipcServer = ipc.IpcServer.listen("bitwarden", (error, msg) => { + switch (msg.kind) { + case ipc.IpcMessageType.Connected: + this.connected.push(msg.clientId); + break; + case ipc.IpcMessageType.Disconnected: { + const index = this.connected.indexOf(msg.clientId); + if (index > -1) { + this.connected.splice(index, 1); + } - ipcMain.on("nativeMessagingReply", (event, msg) => { - if (this.socket != null && msg != null) { - this.send(msg, this.socket); + this.logService.info("client " + index + " has disconnected!"); + break; } - }); - - ipc.server.on("connect", (socket: Socket) => { - this.connected.push(socket); - }); - - ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => { - const index = this.connected.indexOf(socket); - if (index > -1) { - this.connected.splice(index, 1); - } - - this.socket = null; - ipc.log("client " + destroyedSocketID + " has disconnected!"); - }); + case ipc.IpcMessageType.Message: + this.logService.debug("received nativeMessaging message: " + msg.message); + this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message)); + break; + } }); - ipc.server.start(); - } - - stop() { - ipc.server.stop(); - // Kill all existing connections - this.connected.forEach((socket) => { - if (!socket.destroyed) { - socket.destroy(); + ipcMain.on("nativeMessagingReply", (event, msg) => { + if (msg != null) { + this.send(msg); } }); } - send(message: object, socket: any) { - ipc.server.emit(socket, "message", message); + stop() { + this.ipcServer?.stop(); + } + + send(message: object) { + const msg = JSON.stringify(message); + this.logService.debug("sent nativeMessaging message of length", msg.length, message); + this.ipcServer?.send(msg); } async generateManifests() { @@ -331,6 +322,10 @@ export class NativeMessagingMain { } private binaryPath() { + if (isDev()) { + return path.join(this.appPath, "..", "desktop_native", "target", "release", "desktop_proxy"); + } + if (process.platform === "win32") { return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat"); } diff --git a/apps/desktop/src/proxy/ipc.ts b/apps/desktop/src/proxy/ipc.ts deleted file mode 100644 index 0160d6bf294..00000000000 --- a/apps/desktop/src/proxy/ipc.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable no-console */ -import { createHash } from "crypto"; -import { existsSync, mkdirSync } from "fs"; -import { homedir } from "os"; -import { join as path_join } from "path"; - -import * as ipc from "node-ipc"; - -export function getIpcSocketRoot(): string | null { - let socketRoot = null; - - switch (process.platform) { - case "darwin": { - const ipcSocketRootDir = path_join(homedir(), "tmp"); - if (!existsSync(ipcSocketRootDir)) { - mkdirSync(ipcSocketRootDir); - } - socketRoot = ipcSocketRootDir + "/"; - break; - } - case "win32": { - // Let node-ipc use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. - // Hashing prevents problems with reserved characters and file length limitations. - socketRoot = createHash("sha1").update(homedir()).digest("hex") + "."; - } - } - return socketRoot; -} - -ipc.config.id = "proxy"; -ipc.config.retry = 1500; -ipc.config.logger = console.warn; // Stdout is used for native messaging -const ipcSocketRoot = getIpcSocketRoot(); -if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; -} - -export default class IPC { - onMessage: (message: object) => void; - - private connected = false; - - connect() { - ipc.connectTo("bitwarden", () => { - ipc.of.bitwarden.on("connect", () => { - this.connected = true; - console.error("## connected to bitwarden desktop ##"); - - // Notify browser extension, connection is established to desktop application. - this.onMessage({ command: "connected" }); - }); - - ipc.of.bitwarden.on("disconnect", () => { - this.connected = false; - console.error("disconnected from world"); - - // Notify browser extension, no connection to desktop application. - this.onMessage({ command: "disconnected" }); - }); - - ipc.of.bitwarden.on("message", (message: any) => { - this.onMessage(message); - }); - - ipc.of.bitwarden.on("error", (err: any) => { - console.error("error", err); - }); - }); - } - - isConnected(): boolean { - return this.connected; - } - - send(json: object) { - ipc.of.bitwarden.emit("message", json); - } -} diff --git a/apps/desktop/src/proxy/native-messaging-proxy.ts b/apps/desktop/src/proxy/native-messaging-proxy.ts deleted file mode 100644 index f1b54a82014..00000000000 --- a/apps/desktop/src/proxy/native-messaging-proxy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import IPC from "./ipc"; -import NativeMessage from "./nativemessage"; - -// Proxy is a lightweight application which provides bi-directional communication -// between the browser extension and a running desktop application. -// -// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop -export class NativeMessagingProxy { - private ipc: IPC; - private nativeMessage: NativeMessage; - - constructor() { - this.ipc = new IPC(); - this.nativeMessage = new NativeMessage(this.ipc); - } - - run() { - this.ipc.connect(); - this.nativeMessage.listen(); - - this.ipc.onMessage = this.nativeMessage.send; - } -} diff --git a/apps/desktop/src/proxy/nativemessage.ts b/apps/desktop/src/proxy/nativemessage.ts deleted file mode 100644 index f7a32296f84..00000000000 --- a/apps/desktop/src/proxy/nativemessage.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable no-console */ -import IPC from "./ipc"; - -// Mostly based on the example from MDN, -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging -export default class NativeMessage { - ipc: IPC; - - constructor(ipc: IPC) { - this.ipc = ipc; - } - - send(message: object) { - const messageBuffer = Buffer.from(JSON.stringify(message)); - - const headerBuffer = Buffer.alloc(4); - headerBuffer.writeUInt32LE(messageBuffer.length, 0); - - process.stdout.write(Buffer.concat([headerBuffer, messageBuffer])); - } - - listen() { - let payloadSize: number = null; - - // A queue to store the chunks as we read them from stdin. - // This queue can be flushed when `payloadSize` data has been read - const chunks: any = []; - - // Only read the size once for each payload - const sizeHasBeenRead = () => Boolean(payloadSize); - - // All the data has been read, reset everything for the next message - const flushChunksQueue = () => { - payloadSize = null; - chunks.splice(0); - }; - - const processData = () => { - // Create one big buffer with all all the chunks - const stringData = Buffer.concat(chunks); - console.error(stringData); - - // The browser will emit the size as a header of the payload, - // if it hasn't been read yet, do it. - // The next time we'll need to read the payload size is when all of the data - // of the current payload has been read (ie. data.length >= payloadSize + 4) - if (!sizeHasBeenRead()) { - try { - payloadSize = stringData.readUInt32LE(0); - } catch (e) { - console.error(e); - return; - } - } - - // If the data we have read so far is >= to the size advertised in the header, - // it means we have all of the data sent. - // We add 4 here because that's the size of the bytes that old the payloadSize - if (stringData.length >= payloadSize + 4) { - // Remove the header - const contentWithoutSize = stringData.slice(4, payloadSize + 4).toString(); - - // Reset the read size and the queued chunks - flushChunksQueue(); - - const json = JSON.parse(contentWithoutSize); - - // Forward to desktop application - this.ipc.send(json); - } - }; - - process.stdin.on("readable", () => { - // A temporary variable holding the nodejs.Buffer of each - // chunk of data read off stdin - let chunk = null; - - // Read all of the available data - // tslint:disable-next-line:no-conditional-assignment - while ((chunk = process.stdin.read()) !== null) { - chunks.push(chunk); - } - - try { - processData(); - } catch (e) { - console.error(e); - } - }); - - process.stdin.on("end", () => { - process.exit(0); - }); - } -}