[PM-7646][PM-5506] Rust IPC changes: Episode 2 (#11122)

* Revert "[PM-7646][PM-5506] Revert IPC changes (#10946)"

This reverts commit ed4d481e4d.

* Ensure tmp dir gets created on MacOS

* Remove client reconnections

* Improve client error handling and process exiting
This commit is contained in:
Daniel García 2024-10-01 16:28:56 +02:00 committed by GitHub
parent 2b78ac5151
commit 9aeb412404
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1357 additions and 351 deletions

View File

@ -174,20 +174,21 @@ jobs:
with: with:
path: | path: |
apps/desktop/desktop_native/napi/*.node apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
${{ env.RUNNER_TEMP }}/.cargo/registry ${{ env.RUNNER_TEMP }}/.cargo/registry
${{ env.RUNNER_TEMP }}/.cargo/git ${{ env.RUNNER_TEMP }}/.cargo/git
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
env: env:
PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALLOW_CROSS: true
PKG_CONFIG_ALL_STATIC: true PKG_CONFIG_ALL_STATIC: true
TARGET: musl TARGET: musl
run: | run: |
rustup target add x86_64-unknown-linux-musl rustup target add x86_64-unknown-linux-musl
npm run build:cross-platform node build.js cross-platform
- name: Build application - name: Build application
run: npm run dist:lin run: npm run dist:lin
@ -301,13 +302,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: apps/desktop/desktop_native/napi/*.node path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
run: npm run build:cross-platform run: node build.js cross-platform
- name: Build & Sign (dev) - name: Build & Sign (dev)
env: env:
@ -584,13 +587,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: apps/desktop/desktop_native/napi/*.node path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
run: npm run build:cross-platform run: node build.js cross-platform
- name: Build application (dev) - name: Build application (dev)
run: npm run build run: npm run build
@ -748,13 +753,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: apps/desktop/desktop_native/napi/*.node path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
run: npm run build:cross-platform run: node build.js cross-platform
- name: Build - name: Build
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
@ -972,13 +979,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: apps/desktop/desktop_native/napi/*.node path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
run: npm run build:cross-platform run: node build.js cross-platform
- name: Build - name: Build
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
@ -1205,13 +1214,15 @@ jobs:
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: cache id: cache
with: with:
path: apps/desktop/desktop_native/napi/*.node path: |
apps/desktop/desktop_native/napi/*.node
apps/desktop/desktop_native/dist/*
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
- name: Build Native Module - name: Build Native Module
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop/desktop_native/napi working-directory: apps/desktop/desktop_native
run: npm run build:cross-platform run: node build.js cross-platform
- name: Build - name: Build
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'

View File

@ -4,3 +4,4 @@ index.node
**/.DS_Store **/.DS_Store
npm-debug.log* npm-debug.log*
*.node *.node
dist

View File

@ -304,9 +304,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.23" version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -481,6 +481,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "derive-new" name = "derive-new"
version = "0.6.0" version = "0.6.0"
@ -502,10 +511,14 @@ dependencies = [
"base64", "base64",
"cbc", "cbc",
"core-foundation", "core-foundation",
"dirs",
"futures",
"gio", "gio",
"interprocess",
"keytar", "keytar",
"libc", "libc",
"libsecret", "libsecret",
"log",
"rand", "rand",
"retry", "retry",
"scopeguard", "scopeguard",
@ -514,6 +527,7 @@ dependencies = [
"sha2", "sha2",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-util",
"typenum", "typenum",
"widestring", "widestring",
"windows", "windows",
@ -530,6 +544,22 @@ dependencies = [
"napi", "napi",
"napi-build", "napi-build",
"napi-derive", "napi-derive",
"tokio",
"tokio-util",
]
[[package]]
name = "desktop_proxy"
version = "0.0.0"
dependencies = [
"anyhow",
"desktop_core",
"embed_plist",
"futures",
"log",
"simplelog",
"tokio",
"tokio-util",
] ]
[[package]] [[package]]
@ -542,6 +572,27 @@ dependencies = [
"crypto-common", "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]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.2" version = "0.5.2"
@ -551,12 +602,24 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "embed_plist"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]] [[package]]
name = "endi" name = "endi"
version = "1.1.0" version = "1.1.0"
@ -645,6 +708,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.30"
@ -652,6 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -719,6 +798,7 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro", "futures-macro",
@ -913,6 +993,27 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "interprocess"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13"
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]] [[package]]
name = "keytar" name = "keytar"
version = "0.1.6" version = "0.1.6"
@ -950,6 +1051,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "libsecret" name = "libsecret"
version = "0.5.0" version = "0.5.0"
@ -1038,10 +1149,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "napi" name = "mio"
version = "2.16.6" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"bitflags", "bitflags",
"ctor", "ctor",
@ -1059,9 +1181,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
[[package]] [[package]]
name = "napi-derive" name = "napi-derive"
version = "2.16.5" version = "2.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"convert_case", "convert_case",
@ -1130,6 +1252,12 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -1140,6 +1268,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.5" version = "0.3.5"
@ -1257,6 +1394,12 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@ -1366,6 +1509,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.20"
@ -1441,6 +1590,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.7" version = "0.5.7"
@ -1450,6 +1605,17 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.0" version = "1.11.0"
@ -1631,6 +1797,17 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simplelog"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
dependencies = [
"log",
"termcolor",
"time",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -1646,6 +1823,16 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -1724,6 +1911,39 @@ dependencies = [
"syn", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.38.0"
@ -1732,9 +1952,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"libc",
"mio",
"num_cpus", "num_cpus",
"pin-project-lite", "pin-project-lite",
"socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1748,6 +1972,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.19"
@ -2043,6 +2280,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["napi", "core"] members = ["napi", "core", "proxy"]

View File

@ -0,0 +1,68 @@
/* 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, release = true) {
const targetArg = target ? `--target ${target}` : "";
const releaseArg = release ? "--release" : "";
return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") });
}
function buildProxyBin(target, release = true) {
const targetArg = target ? `--target ${target}` : "";
const releaseArg = release ? "--release" : "";
return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
}
if (!crossPlatform) {
console.log("Building native modules in debug mode for the native architecture");
buildNapiModule(false, false);
buildProxyBin(false, false);
return;
}
// Note that targets contains pairs of [rust target, node arch]
// We do this to move the output binaries to a location that can
// be easily accessed from electron-builder using ${os} and ${arch}
let targets = [];
switch (process.platform) {
case "win32":
targets = [
["i686-pc-windows-msvc", 'ia32'],
["x86_64-pc-windows-msvc", 'x64'],
["aarch64-pc-windows-msvc", 'arm64']
];
break;
case "darwin":
targets = [
["x86_64-apple-darwin", 'x64'],
["aarch64-apple-darwin", 'arm64']
];
break;
default:
targets = [
['x86_64-unknown-linux-musl', 'x64']
];
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
break;
}
console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", "));
fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true });
targets.forEach(([target, nodeArch]) => {
buildNapiModule(target);
buildProxyBin(target);
const ext = process.platform === "win32" ? ".exe" : "";
fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`));
});

View File

@ -6,9 +6,21 @@ version = "0.0.0"
publish = false publish = false
[features] [features]
default = [] default = ["sys"]
manual_test = [] manual_test = []
sys = [
"dep:widestring",
"dep:windows",
"dep:core-foundation",
"dep:security-framework",
"dep:security-framework-sys",
"dep:gio",
"dep:libsecret",
"dep:zbus",
"dep:zbus_polkit",
]
[dependencies] [dependencies]
aes = "=0.8.4" aes = "=0.8.4"
anyhow = "=1.0.86" anyhow = "=1.0.86"
@ -17,17 +29,22 @@ arboard = { version = "=3.4.1", default-features = false, features = [
] } ] }
base64 = "=0.22.1" base64 = "=0.22.1"
cbc = { version = "=0.1.2", features = ["alloc"] } cbc = { version = "=0.1.2", features = ["alloc"] }
dirs = "=5.0.1"
futures = "=0.3.30"
interprocess = { version = "=2.2.1", features = ["tokio"] }
libc = "=0.2.155" libc = "=0.2.155"
log = "=0.4.22"
rand = "=0.8.5" rand = "=0.8.5"
retry = "=2.0.0" retry = "=2.0.0"
scopeguard = "=1.2.0" scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"
thiserror = "=1.0.61" thiserror = "=1.0.61"
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.11"
typenum = "=1.17.0" typenum = "=1.17.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
widestring = "=1.1.0" widestring = { version = "=1.1.0", optional = true }
windows = { version = "=0.57.0", features = [ windows = { version = "=0.57.0", features = [
"Foundation", "Foundation",
"Security_Credentials_UI", "Security_Credentials_UI",
@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [
"Win32_System_WinRT", "Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
] } ], optional = true }
[target.'cfg(windows)'.dev-dependencies] [target.'cfg(windows)'.dev-dependencies]
keytar = "=0.1.6" keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "=0.9.4" core-foundation = { version = "=0.9.4", optional = true }
security-framework = "=2.11.0" security-framework = { version = "=2.11.0", optional = true }
security-framework-sys = "=2.11.0" security-framework-sys = { version = "=2.11.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gio = "=0.19.5" gio = { version = "=0.19.5", optional = true }
libsecret = "=0.5.0" libsecret = { version = "=0.5.0", optional = true }
zbus = "=4.3.1" zbus = { version = "=4.3.1", optional = true }
zbus_polkit = "=4.0.0" zbus_polkit = { version = "=4.0.0", optional = true }

View File

@ -0,0 +1,70 @@
use std::path::PathBuf;
use interprocess::local_socket::{
tokio::{prelude::*, Stream},
GenericFilePath, ToFsName,
};
use log::{error, info};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE;
pub async fn connect(
path: PathBuf,
send: tokio::sync::mpsc::Sender<String>,
mut recv: tokio::sync::mpsc::Receiver<String>,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Attempting to connect to {}", path.display());
let name = path.as_os_str().to_fs_name::<GenericFilePath>()?;
let mut conn = Stream::connect(name).await?;
info!("Connected to {}", path.display());
// This `connected` and the latter `disconnected` messages are the only ones that
// are sent from the Rust IPC code and not just forwarded from the desktop app.
// As it's only two, we hardcode the JSON values to avoid pulling in a JSON library.
send.send("{\"command\":\"connected\"}".to_owned()).await?;
let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE];
// Listen to IPC messages
loop {
tokio::select! {
// Forward messages to the IPC server
msg = recv.recv() => {
match msg {
Some(msg) => {
conn.write_all(msg.as_bytes()).await?;
}
None => {
info!("Client channel closed");
break;
},
}
},
// Forward messages from the IPC server
res = conn.read(&mut buffer[..]) => {
match res {
Err(e) => {
error!("Error reading from IPC server: {e}");
break;
}
Ok(0) => {
info!("Connection closed");
break;
}
Ok(n) => {
let message = String::from_utf8_lossy(&buffer[..n]).to_string();
send.send(message).await?;
}
}
}
}
}
let _ = send.send("{\"command\":\"disconnected\"}".to_owned()).await;
Ok(())
}

View File

@ -0,0 +1,66 @@
pub mod client;
pub mod server;
/// The maximum size of a message that can be sent over IPC.
/// According to the documentation, the maximum size sent to the browser is 1MB.
/// While the maximum size sent from the browser to the native messaging host is 4GB.
///
/// Currently we are setting the maximum both ways to be 1MB.
///
/// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side
/// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024;
/// The maximum number of messages that can be buffered in a channel.
/// This number is more or less arbitrary and can be adjusted as needed,
/// but ideally the messages should be processed as quickly as possible.
pub const MESSAGE_CHANNEL_BUFFER: usize = 32;
/// Resolve the path to the IPC socket.
pub fn path(name: &str) -> std::path::PathBuf {
#[cfg(target_os = "windows")]
{
// Use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden 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 home = dirs::home_dir().unwrap();
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()
}
#[cfg(target_os = "macos")]
{
let mut home = dirs::home_dir().unwrap();
// When running in an unsandboxed environment, path is: /Users/<user>/
// While running sandboxed, it's different: /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
//
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
// so we need to remove all the components after the user.
// Note that we subtract 3 because the root directory is counted as a component (/, Users, <user>).
let num_components = home.components().count();
for _ in 0..num_components - 3 {
home.pop();
}
let tmp = home.join("Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp");
// The tmp directory might not exist, so create it
let _ = std::fs::create_dir_all(&tmp);
tmp.join(format!("app.{name}"))
}
#[cfg(target_os = "linux")]
{
// On Linux, we use the user's cache directory.
let home = dirs::cache_dir().unwrap();
let path_dir = home.join("com.bitwarden.desktop");
// The chache directory might not exist, so create it
let _ = std::fs::create_dir_all(&path_dir);
path_dir.join(format!("app.{name}"))
}
}

View File

@ -0,0 +1,232 @@
use std::{error::Error, path::Path, vec};
use futures::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, mpsc},
};
use tokio_util::sync::CancellationToken;
use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
#[derive(Debug)]
pub struct Message {
pub client_id: u32,
pub kind: MessageType,
// This value should be Some for MessageType::Message and None for the rest
pub message: Option<String>,
}
#[derive(Debug)]
pub enum MessageType {
Connected,
Disconnected,
Message,
}
pub struct Server {
cancel_token: CancellationToken,
server_to_clients_send: broadcast::Sender<String>,
}
impl Server {
/// Create and start the IPC server without blocking.
///
/// # Parameters
///
/// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// - `client_to_server_send`: This [`mpsc::Sender<Message>`] will receive all the [`Message`]'s that the clients send to this server.
pub fn start(
path: &Path,
client_to_server_send: mpsc::Sender<Message>,
) -> Result<Self, Box<dyn Error>> {
// 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 should remain connected to it 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::<GenericFilePath>()?;
let opts = ListenerOptions::new().name(name);
let listener = opts.create_tokio()?;
// This broadcast channel is used for sending messages to all connected clients, and so the sender
// will be stored in the server while the receiver will be cloned and passed to each client handler.
let (server_to_clients_send, server_to_clients_recv) =
broadcast::channel::<String>(MESSAGE_CHANNEL_BUFFER);
// This cancellation token allows us to cleanly stop the server and all the spawned
// tasks without having to wait on all the pending tasks finalizing first
let cancel_token = CancellationToken::new();
// Create the server and start listening for incoming connections
// in a separate task to avoid blocking the current task
let server = Server {
cancel_token: cancel_token.clone(),
server_to_clients_send,
};
tokio::spawn(listen_incoming(
listener,
client_to_server_send,
server_to_clients_recv,
cancel_token,
));
Ok(server)
}
/// Send a message over the IPC server to all the connected clients
///
/// # Returns
///
/// The number of clients that the message was sent to. Note that the number of messages
/// sent may be less than the number of connected clients if some clients disconnect while
/// the message is being sent.
pub fn send(&self, message: String) -> Result<usize> {
let sent = self.server_to_clients_send.send(message)?;
Ok(sent)
}
/// Stop the IPC server.
pub fn stop(&self) {
self.cancel_token.cancel();
}
}
impl Drop for Server {
fn drop(&mut self) {
self.stop();
}
}
async fn listen_incoming(
listener: LocalSocketListener,
client_to_server_send: mpsc::Sender<Message>,
server_to_clients_recv: broadcast::Receiver<String>,
cancel_token: CancellationToken,
) {
// We use a simple incrementing ID for each client
let mut next_client_id = 1_u32;
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
info!("IPC server cancelled.");
break;
},
// A new client connection has been established
msg = listener.accept() => {
match msg {
Ok(client_stream) => {
let client_id = next_client_id;
next_client_id += 1;
let future = handle_connection(
client_stream,
client_to_server_send.clone(),
// We resubscribe to the receiver here so this task can have it's own copy
// Note that this copy will only receive messages sent after this point,
// but that is okay, realistically we don't want any messages before we get a chance
// to send the connected message to the client, which is done inside [`handle_connection`]
server_to_clients_recv.resubscribe(),
cancel_token.clone(),
client_id
);
tokio::spawn(future.map_err(|e| {
error!("Error handling connection: {}", e)
}));
},
Err(e) => {
error!("Error accepting connection: {}", e);
break;
},
}
}
}
}
}
async fn handle_connection(
mut client_stream: impl AsyncRead + AsyncWrite + Unpin,
client_to_server_send: mpsc::Sender<Message>,
mut server_to_clients_recv: broadcast::Receiver<String>,
cancel_token: CancellationToken,
client_id: u32,
) -> Result<(), Box<dyn Error>> {
client_to_server_send
.send(Message {
client_id,
kind: MessageType::Connected,
message: None,
})
.await?;
let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE];
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
info!("Client {client_id} cancelled.");
break;
},
// Forward messages to the IPC clients
msg = server_to_clients_recv.recv() => {
match msg {
Ok(msg) => {
client_stream.write_all(msg.as_bytes()).await?;
},
Err(e) => {
info!("Error reading message: {}", e);
break;
}
}
},
// Forwards messages from the IPC clients to the server
// Note that we also send connect and disconnect events so that
// the server can keep track of multiple clients
result = client_stream.read(&mut buf) => {
match result {
Err(e) => {
info!("Error reading from client {client_id}: {e}");
client_to_server_send.send(Message {
client_id,
kind: MessageType::Disconnected,
message: None,
}).await?;
break;
},
Ok(0) => {
info!("Client {client_id} disconnected.");
client_to_server_send.send(Message {
client_id,
kind: MessageType::Disconnected,
message: None,
}).await?;
break;
},
Ok(size) => {
let msg = std::str::from_utf8(&buf[..size])?;
client_to_server_send.send(Message {
client_id,
kind: MessageType::Message,
message: Some(msg.to_string()),
}).await?;
},
}
}
}
}
Ok(())
}

View File

@ -1,7 +1,13 @@
#[cfg(feature = "sys")]
pub mod biometric; pub mod biometric;
#[cfg(feature = "sys")]
pub mod clipboard; pub mod clipboard;
pub mod crypto; pub mod crypto;
pub mod error; pub mod error;
pub mod ipc;
#[cfg(feature = "sys")]
pub mod password; pub mod password;
#[cfg(feature = "sys")]
pub mod process_isolation; pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod powermonitor; pub mod powermonitor;

View File

@ -16,8 +16,10 @@ manual_test = []
[dependencies] [dependencies]
anyhow = "=1.0.86" anyhow = "=1.0.86"
desktop_core = { path = "../core" } desktop_core = { path = "../core" }
napi = { version = "=2.16.6", features = ["async"] } napi = { version = "=2.16.7", features = ["async"] }
napi-derive = "=2.16.5" napi-derive = "=2.16.6"
tokio = { version = "1.38.0" }
tokio-util = "0.7.11"
[build-dependencies] [build-dependencies]
napi-build = "=2.1.3" napi-build = "=2.1.3"

View File

@ -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'});
});

View File

@ -51,3 +51,33 @@ export namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void> export function onLock(callback: (err: Error | null, ) => any): Promise<void>
export function isLockMonitorAvailable(): Promise<boolean> export function isLockMonitorAvailable(): Promise<boolean>
} }
export namespace ipc {
export interface IpcMessage {
clientId: number
kind: IpcMessageType
message?: string
}
export const enum IpcMessageType {
Connected = 0,
Disconnected = 1,
Message = 2
}
export class IpcServer {
/**
* Create and start the IPC server without blocking.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
* @param callback This function will be called whenever a message is received from a client.
*/
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
/** Stop the IPC server. */
stop(): void
/**
* Send a message over the IPC server to all the connected clients
*
* @return The number of clients that the message was sent to. Note that the number of messages
* actually received may be less, as some clients could disconnect before receiving the message.
*/
send(message: string): number
}
}

View File

@ -206,10 +206,4 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding module.exports = nativeBinding
module.exports.passwords = passwords
module.exports.biometrics = biometrics
module.exports.clipboards = clipboards
module.exports.processisolations = processisolations
module.exports.powermonitors = powermonitors

View File

@ -3,9 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "napi build --release --platform --js false", "build": "napi build --platform --js false",
"build:debug": "napi build --platform --js false",
"build:cross-platform": "node build.js",
"test": "cargo test" "test": "cargo test"
}, },
"author": "", "author": "",

View File

@ -189,3 +189,103 @@ pub mod powermonitors {
} }
} }
#[napi]
pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
#[napi(object)]
pub struct IpcMessage {
pub client_id: u32,
pub kind: IpcMessageType,
pub message: Option<String>,
}
impl From<Message> 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<MessageType> 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 without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
#[napi(factory)]
pub async fn listen(
name: String,
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
while let Some(message) = recv.recv().await {
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
}
});
let path = desktop_core::ipc::path(&name);
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
napi::Error::from_reason(format!(
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
))
})?;
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 to all the connected clients
///
/// @return The number of clients that the message was sent to. Note that the number of messages
/// actually received may be less, as some clients could disconnect before receiving the message.
#[napi]
pub fn send(&self, message: String) -> napi::Result<u32> {
self.server
.send(message)
.map_err(|e| {
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
})
// NAPI doesn't support u64 or usize, so we need to convert to u32
.map(|u| u32::try_from(u).unwrap_or_default())
}
}
}

View File

@ -0,0 +1,19 @@
[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", default-features = false }
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"] }
[target.'cfg(target_os = "macos")'.dependencies]
embed_plist = "1.2.2"

View File

@ -0,0 +1,159 @@
use std::path::Path;
use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE};
use futures::{FutureExt, SinkExt, StreamExt};
use log::*;
use tokio_util::codec::LengthDelimitedCodec;
#[cfg(target_os = "macos")]
embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist");
fn init_logging(log_path: &Path, level: log::LevelFilter) {
use simplelog::{ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode};
let config = Config::default();
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
loggers.push(TermLogger::new(
level,
config.clone(),
TerminalMode::Stderr,
ColorChoice::Auto,
));
match std::fs::File::create(log_path) {
Ok(file) => {
loggers.push(simplelog::WriteLogger::new(level, config, file));
}
Err(e) => {
eprintln!("Can't create file: {}", e);
}
}
if let Err(e) = CombinedLogger::init(loggers) {
eprintln!("Failed to initialize logger: {}", e);
}
}
/// 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() {
let sock_path = desktop_core::ipc::path("bitwarden");
let log_path = {
let mut path = sock_path.clone();
path.set_extension("bitwarden.log");
path
};
init_logging(&log_path, LevelFilter::Info);
info!("Starting Bitwarden IPC Proxy.");
// Different browsers send different arguments when the app starts:
//
// Firefox:
// - The complete path to the app manifest. (in the form `/Users/<user>/Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`)
// - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`).
//
// Chrome on Windows:
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
// - Handle to the Chrome native window that started the app.
//
// Chrome on Linux and Mac:
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
let args: Vec<_> = std::env::args().skip(1).collect();
info!("Process args: {:?}", args);
// Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`)
let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
let mut handle = tokio::spawn(
desktop_core::ipc::client::connect(sock_path, out_send, in_recv)
.map(|r| r.map_err(|e| e.to_string())),
);
// Create a new codec for reading and writing messages from stdin/stdout.
let mut stdin = LengthDelimitedCodec::builder()
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
.native_endian()
.new_read(tokio::io::stdin());
let mut stdout = LengthDelimitedCodec::builder()
.max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE)
.native_endian()
.new_write(tokio::io::stdout());
loop {
tokio::select! {
// This forces tokio to poll the futures in the order that they are written.
// We want the spawn handle to be evaluated first so that we can get any error
// results before we get the channel closed message.
biased;
// IPC client has finished, so we should exit as well.
res = &mut handle => {
match res {
Ok(Ok(())) => {
info!("IPC client finished successfully.");
std::process::exit(0);
}
Ok(Err(e)) => {
error!("IPC client connection error: {}", e);
std::process::exit(1);
}
Err(e) => {
error!("IPC client spawn error: {}", e);
std::process::exit(1);
}
}
}
// Receive messages from IPC and print to STDOUT.
msg = out_recv.recv() => {
match msg {
Some(msg) => {
debug!("OUT: {}", msg);
stdout.send(msg.into()).await.unwrap();
}
None => {
info!("Channel closed, exiting.");
std::process::exit(0);
}
}
},
// 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_send.send(m).await.unwrap();
}
Some(Err(e)) => {
error!("Error parsing input: {}", e);
std::process::exit(1);
}
None => {
info!("Received EOF, exiting.");
std::process::exit(0);
}
}
}
}
}
}

View File

@ -73,6 +73,13 @@
"CFBundleDevelopmentRegion": "en" "CFBundleDevelopmentRegion": "en"
}, },
"singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node",
"extraFiles": [
{
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
"to": "MacOS/desktop_proxy"
}
],
"signIgnore": ["MacOS/desktop_proxy"],
"target": ["dmg", "zip"] "target": ["dmg", "zip"]
}, },
"win": { "win": {
@ -84,16 +91,24 @@
"from": "../../node_modules/regedit/vbs", "from": "../../node_modules/regedit/vbs",
"to": "regedit/vbs", "to": "regedit/vbs",
"filter": ["**/*"] "filter": ["**/*"]
}, }
],
"extraFiles": [
{ {
"from": "resources/native-messaging.bat", "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
"to": "native-messaging.bat" "to": "desktop_proxy.exe"
} }
] ]
}, },
"linux": { "linux": {
"category": "Utility", "category": "Utility",
"synopsis": "A secure and free password manager for all of your devices.", "synopsis": "A secure and free password manager for all of your devices.",
"extraFiles": [
{
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
"to": "desktop_proxy"
}
],
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"], "target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
"desktop": { "desktop": {
"Name": "Bitwarden", "Name": "Bitwarden",

View File

@ -18,7 +18,7 @@
"scripts": { "scripts": {
"postinstall": "electron-rebuild", "postinstall": "electron-rebuild",
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", "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": "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: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", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
</dict>
</plist>

View File

@ -8,6 +8,10 @@
<string>LTZ2PFU5D6</string> <string>LTZ2PFU5D6</string>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.application-groups</key>
<array>
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
</array>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.security.files.user-selected.read-write</key>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.bitwarden.desktop</string>
</dict>
</plist>

View File

@ -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 %*

View File

@ -1,14 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires, no-console */ /* eslint-disable @typescript-eslint/no-var-requires, no-console */
require("dotenv").config(); require("dotenv").config();
const child_process = require("child_process");
const path = require("path"); const path = require("path");
const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses");
const builder = require("electron-builder");
const fse = require("fs-extra"); const fse = require("fs-extra");
exports.default = run; exports.default = run;
async function run(context) { async function run(context) {
console.log("## After pack"); console.log("## After pack");
console.log(context); // console.log(context);
if (context.packager.platform.nodeName !== "darwin" || context.arch === builder.Arch.universal) {
await addElectronFuses(context);
}
if (context.electronPlatformName === "linux") { if (context.electronPlatformName === "linux") {
console.log("Creating memory-protection wrapper script"); console.log("Creating memory-protection wrapper script");
const appOutDir = context.appOutDir; const appOutDir = context.appOutDir;
@ -23,4 +31,132 @@ async function run(context) {
fse.chmodSync(wrapperBin, "755"); fse.chmodSync(wrapperBin, "755");
console.log("Copied memory-protection wrapper script"); console.log("Copied memory-protection wrapper script");
} }
if (["darwin", "mas"].includes(context.electronPlatformName)) {
const is_mas = context.electronPlatformName === "mas";
const is_mas_dev = context.targets.some((e) => e.name === "mas-dev");
let id;
// Only use the Bitwarden Identities on CI
if (process.env.GITHUB_ACTIONS === "true") {
if (is_mas) {
id = is_mas_dev
? "E7C9978F6FBCE0553429185C405E61F5380BE8EB"
: "3rd Party Mac Developer Application: Bitwarden Inc";
} else {
id = "Developer ID Application: 8bit Solutions LLC";
}
// Locally, use the first valid code signing identity, unless CSC_NAME is set
} else if (process.env.CSC_NAME) {
id = process.env.CSC_NAME;
} else {
const identities = getIdentities();
if (identities.length === 0) {
throw new Error("No valid identities found");
}
id = identities[0].id;
}
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`);
const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`;
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
const packageId = "com.bitwarden.desktop";
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
}
}
// Partially based on electron-builder code:
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/macPackager.ts
// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/macCodeSign.ts
const appleCertificatePrefixes = [
"Developer ID Application:",
// "Developer ID Installer:",
// "3rd Party Mac Developer Application:",
// "3rd Party Mac Developer Installer:",
"Apple Development:",
];
function getIdentities() {
const ids = child_process
.execSync("/usr/bin/security find-identity -v -p codesigning")
.toString();
return ids
.split("\n")
.filter((line) => {
for (const prefix of appleCertificatePrefixes) {
if (line.includes(prefix)) {
return true;
}
}
return false;
})
.map((line) => {
const split = line.trim().split(" ");
const id = split[1];
const name = split.slice(2).join(" ").replace(/"/g, "");
return { id, name };
});
}
/**
* @param {import("electron-builder").AfterPackContext} context
*/
async function addElectronFuses(context) {
const platform = context.packager.platform.nodeName;
const ext = {
darwin: ".app",
win32: ".exe",
linux: "",
}[platform];
const IS_LINUX = platform === "linux";
const executableName = IS_LINUX
? context.packager.appInfo.productFilename.toLowerCase().replace("-dev", "").replace(" ", "-")
: context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds
const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`);
console.log("## Adding fuses to the electron binary", electronBinaryPath);
await flipFuses(electronBinaryPath, {
version: FuseVersion.V1,
strictlyRequireAllFuses: true,
resetAdHocDarwinSignature: platform === "darwin" && context.arch === builder.Arch.universal,
// List of fuses and their default values is available at:
// https://www.electronjs.org/docs/latest/tutorial/fuses
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
// Currently, asar integrity is only implemented for macOS and Windows
// https://www.electronjs.org/docs/latest/tutorial/asar-integrity
// On macOS, it works by default, but on Windows it requires the
// asarIntegrity feature of electron-builder v25, currently in alpha
// https://github.com/electron-userland/electron-builder/releases/tag/v25.0.0-alpha.10
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: platform === "darwin",
[FuseV1Options.OnlyLoadAppFromAsar]: true,
// App refuses to open when enabled
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
// To disable this, we should stop using the file:// protocol to load the app bundle
// This can be done by defining a custom app:// protocol and loading the bundle from there,
// but then any requests to the server will be blocked by CORS policy
[FuseV1Options.GrantFileProtocolExtraPrivileges]: true,
});
} }

View File

@ -1,31 +1,33 @@
import { NativeMessagingProxy } from "./proxy/native-messaging-proxy"; import { spawn } from "child_process";
import * as path from "path";
// We need to import the other dependencies using `require` since `import` will import { app } from "electron";
// 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 ( if (
process.platform === "darwin" &&
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1) process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
) { ) {
if (process.platform === "darwin") { // If we're on MacOS, we need to support DuckDuckGo's IPC communication,
// eslint-disable-next-line // which for the moment is launching the Bitwarden process.
const app = require("electron").app; // Ideally the browser would instead startup the desktop_proxy process
// when available, but for now we'll just launch it here.
app.on("ready", () => { app.on("ready", () => {
app.dock.hide(); app.dock.hide();
});
}
process.stdout.on("error", (e) => {
if (e.code === "EPIPE") {
process.exit(0);
}
}); });
const proxy = new NativeMessagingProxy(); const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), {
proxy.run(); cwd: process.cwd(),
stdio: "inherit",
shell: false,
});
proc.on("exit", () => {
process.exit(0);
});
proc.on("error", () => {
process.exit(1);
});
} else { } else {
// eslint-disable-next-line // eslint-disable-next-line
const Main = require("./main").Main; const Main = require("./main").Main;

View File

@ -227,6 +227,7 @@ export class Main {
this.windowMain, this.windowMain,
app.getPath("userData"), app.getPath("userData"),
app.getPath("exe"), app.getPath("exe"),
app.getAppPath(),
); );
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
@ -273,13 +274,21 @@ export class Main {
if (browserIntegrationEnabled || ddgIntegrationEnabled) { if (browserIntegrationEnabled || ddgIntegrationEnabled) {
// Re-register the native messaging host integrations on startup, in case they are not present // Re-register the native messaging host integrations on startup, in case they are not present
if (browserIntegrationEnabled) { if (browserIntegrationEnabled) {
this.nativeMessagingMain.generateManifests().catch(this.logService.error); this.nativeMessagingMain
.generateManifests()
.catch((err) => this.logService.error("Error while generating manifests", err));
} }
if (ddgIntegrationEnabled) { if (ddgIntegrationEnabled) {
this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); this.nativeMessagingMain
.generateDdgManifests()
.catch((err) => this.logService.error("Error while generating DDG manifests", err));
} }
this.nativeMessagingMain.listen(); this.nativeMessagingMain
.listen()
.catch((err) =>
this.logService.error("Error while starting native message listener", err),
);
} }
app.removeAsDefaultProtocolClient("bitwarden"); app.removeAsDefaultProtocolClient("bitwarden");

View File

@ -1,34 +1,34 @@
import { existsSync, promises as fs } from "fs"; import { existsSync, promises as fs } from "fs";
import { Socket } from "net";
import { homedir, userInfo } from "os"; import { homedir, userInfo } from "os";
import * as path from "path"; import * as path from "path";
import * as util from "util"; import * as util from "util";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import * as ipc from "node-ipc";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; 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"; import { WindowMain } from "./window.main";
export class NativeMessagingMain { export class NativeMessagingMain {
private connected: Socket[] = []; private ipcServer: ipc.IpcServer | null;
private socket: any; private connected: number[] = [];
constructor( constructor(
private logService: LogService, private logService: LogService,
private windowMain: WindowMain, private windowMain: WindowMain,
private userPath: string, private userPath: string,
private exePath: string, private exePath: string,
private appPath: string,
) { ) {
ipcMain.handle( ipcMain.handle(
"nativeMessaging.manifests", "nativeMessaging.manifests",
async (_event: any, options: { create: boolean }) => { async (_event: any, options: { create: boolean }) => {
if (options.create) { if (options.create) {
this.listen();
try { try {
await this.listen();
await this.generateManifests(); await this.generateManifests();
} catch (e) { } catch (e) {
this.logService.error("Error generating manifests: " + e); this.logService.error("Error generating manifests: " + e);
@ -51,8 +51,8 @@ export class NativeMessagingMain {
"nativeMessaging.ddgManifests", "nativeMessaging.ddgManifests",
async (_event: any, options: { create: boolean }) => { async (_event: any, options: { create: boolean }) => {
if (options.create) { if (options.create) {
this.listen();
try { try {
await this.listen();
await this.generateDdgManifests(); await this.generateDdgManifests();
} catch (e) { } catch (e) {
this.logService.error("Error generating duckduckgo manifests: " + e); this.logService.error("Error generating duckduckgo manifests: " + e);
@ -72,56 +72,46 @@ export class NativeMessagingMain {
); );
} }
listen() { async listen() {
ipc.config.id = "bitwarden"; if (this.ipcServer) {
ipc.config.retry = 1500; this.ipcServer.stop();
const ipcSocketRoot = getIpcSocketRoot();
if (ipcSocketRoot != null) {
ipc.config.socketRoot = ipcSocketRoot;
} }
ipc.serve(() => { this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => {
ipc.server.on("message", (data: any, socket: any) => { switch (msg.kind) {
this.socket = socket; case ipc.IpcMessageType.Connected: {
this.windowMain.win.webContents.send("nativeMessaging", data); this.connected.push(msg.clientId);
}); this.logService.info("Native messaging client " + msg.clientId + " has connected");
break;
ipcMain.on("nativeMessagingReply", (event, msg) => {
if (this.socket != null && msg != null) {
this.send(msg, this.socket);
} }
}); case ipc.IpcMessageType.Disconnected: {
const index = this.connected.indexOf(msg.clientId);
if (index > -1) {
this.connected.splice(index, 1);
}
ipc.server.on("connect", (socket: Socket) => { this.logService.info("Native messaging client " + msg.clientId + " has disconnected");
this.connected.push(socket); break;
});
ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => {
const index = this.connected.indexOf(socket);
if (index > -1) {
this.connected.splice(index, 1);
} }
case ipc.IpcMessageType.Message:
this.socket = null; this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message));
ipc.log("client " + destroyedSocketID + " has disconnected!"); break;
}); }
}); });
ipc.server.start(); ipcMain.on("nativeMessagingReply", (event, msg) => {
} if (msg != null) {
this.send(msg);
stop() {
ipc.server.stop();
// Kill all existing connections
this.connected.forEach((socket) => {
if (!socket.destroyed) {
socket.destroy();
} }
}); });
} }
send(message: object, socket: any) { stop() {
ipc.server.emit(socket, "message", message); this.ipcServer?.stop();
}
send(message: object) {
this.ipcServer?.send(JSON.stringify(message));
} }
async generateManifests() { async generateManifests() {
@ -331,11 +321,20 @@ export class NativeMessagingMain {
} }
private binaryPath() { private binaryPath() {
if (process.platform === "win32") { const ext = process.platform === "win32" ? ".exe" : "";
return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat");
if (isDev()) {
return path.join(
this.appPath,
"..",
"desktop_native",
"target",
"debug",
`desktop_proxy${ext}`,
);
} }
return this.exePath; return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
} }
private getRegeditInstance() { private getRegeditInstance() {

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
});
}
}

28
package-lock.json generated
View File

@ -24,6 +24,7 @@
"@angular/platform-browser": "16.2.12", "@angular/platform-browser": "16.2.12",
"@angular/platform-browser-dynamic": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12",
"@angular/router": "16.2.12", "@angular/router": "16.2.12",
"@electron/fuses": "1.8.0",
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",
"@koa/router": "12.0.1", "@koa/router": "12.0.1",
"@microsoft/signalr": "8.0.7", "@microsoft/signalr": "8.0.7",
@ -5126,6 +5127,33 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/@electron/fuses": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dependencies": {
"chalk": "^4.1.1",
"fs-extra": "^9.0.1",
"minimist": "^1.2.5"
},
"bin": {
"electron-fuses": "dist/bin.js"
}
},
"node_modules/@electron/fuses/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@electron/get": { "node_modules/@electron/get": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",

View File

@ -157,6 +157,7 @@
"@angular/platform-browser": "16.2.12", "@angular/platform-browser": "16.2.12",
"@angular/platform-browser-dynamic": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12",
"@angular/router": "16.2.12", "@angular/router": "16.2.12",
"@electron/fuses": "1.8.0",
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",
"@koa/router": "12.0.1", "@koa/router": "12.0.1",
"@microsoft/signalr": "8.0.7", "@microsoft/signalr": "8.0.7",