Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8cb0c5fc | ||
|
|
c41e917d4e | ||
|
|
4d43a6270a | ||
|
|
7543c3d35b | ||
|
|
bd383e51f0 | ||
|
|
fb9407cbcb | ||
|
|
9903175a51 | ||
|
|
af55da7bbd | ||
|
|
875f93019b | ||
|
|
73bcc78e12 | ||
|
|
d0b3edc0f1 | ||
|
|
ec67c55017 | ||
|
|
ee47959258 | ||
|
|
7b399f26d8 | ||
|
|
f70d1faaa0 | ||
|
|
b8f2aaf5dc | ||
|
|
2af375875b | ||
|
|
6e5a0466b3 | ||
|
|
2486ffe4e2 | ||
|
|
9adf4191f0 | ||
|
|
4fe3cbf6b6 | ||
|
|
a360c14a58 | ||
|
|
d33445757d | ||
|
|
2c7c0d16dc | ||
|
|
9be982cbf3 | ||
|
|
8b4eff395e | ||
|
|
a84b3560d1 | ||
|
|
36add9ba9b |
2
.github/workflows/build_and_test.yml
vendored
2
.github/workflows/build_and_test.yml
vendored
@ -589,7 +589,7 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
||||
- run: npm ci
|
||||
|
||||
2
.github/workflows/npm.yml
vendored
2
.github/workflows/npm.yml
vendored
@ -154,7 +154,7 @@ jobs:
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
include = ["Cargo.toml", "rust/**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
reorder_keys = false
|
||||
align_comments = false
|
||||
indent_string = ' '
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "workspace.dependencies", "dev-dependencies", "build-dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = true
|
||||
inline_table_expand = false
|
||||
reorder_keys = true
|
||||
|
||||
150
Cargo.lock
generated
150
Cargo.lock
generated
@ -2265,9 +2265,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libcrux-hacl-rs"
|
||||
@ -2494,6 +2494,7 @@ dependencies = [
|
||||
"libsignal-message-backup",
|
||||
"libsignal-net",
|
||||
"libsignal-net-chat",
|
||||
"libsignal-net-grpc",
|
||||
"libsignal-protocol",
|
||||
"linkme",
|
||||
"neon",
|
||||
@ -2608,14 +2609,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-debug"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-ffi"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"hex",
|
||||
@ -2636,7 +2637,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-jni"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"libsignal-debug",
|
||||
"libsignal-jni-impl",
|
||||
@ -2644,7 +2645,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-jni-impl"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
@ -2661,7 +2662,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-jni-testing"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"jni 0.21.1",
|
||||
"libsignal-bridge-testing",
|
||||
@ -2795,6 +2796,7 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itertools 0.14.0",
|
||||
@ -2880,6 +2882,7 @@ dependencies = [
|
||||
"rand 0.9.4",
|
||||
"rand_chacha",
|
||||
"ref-cast",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@ -2905,9 +2908,15 @@ name = "libsignal-net-grpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"const-str",
|
||||
"derive-where",
|
||||
"libsignal-core",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"pbjson-types",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
@ -2977,12 +2986,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-node"
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"libsignal-bridge",
|
||||
"libsignal-bridge-macros",
|
||||
"libsignal-bridge-testing",
|
||||
"libsignal-bridge-types",
|
||||
"libsignal-protocol",
|
||||
"linkme",
|
||||
"log",
|
||||
@ -2996,6 +3006,19 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-node-native_ts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"libsignal-bridge",
|
||||
"libsignal-bridge-testing",
|
||||
"libsignal-bridge-types",
|
||||
"libsignal-node",
|
||||
"minijinja",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-protocol"
|
||||
version = "0.1.0"
|
||||
@ -3197,6 +3220,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memo-map"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@ -3282,6 +3311,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "2.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"memo-map",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@ -3300,9 +3340,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
@ -4882,12 +4922,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5261,9 +5301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
version = "1.52.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@ -5286,9 +5326,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -6050,15 +6090,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@ -6092,30 +6123,13 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@ -6128,12 +6142,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@ -6146,12 +6154,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@ -6164,24 +6166,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@ -6194,12 +6184,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@ -6212,12 +6196,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@ -6230,12 +6208,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@ -6248,12 +6220,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
|
||||
@ -22,6 +22,7 @@ members = [
|
||||
"rust/bridge/jni/impl",
|
||||
"rust/bridge/jni/testing",
|
||||
"rust/bridge/node",
|
||||
"rust/bridge/node/native_ts",
|
||||
]
|
||||
default-members = [
|
||||
"rust/crypto",
|
||||
@ -38,7 +39,7 @@ default-members = [
|
||||
resolver = "2" # so that our dev-dependency features don't leak into products
|
||||
|
||||
[workspace.package]
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
authors = ["Signal Messenger LLC"]
|
||||
license = "AGPL-3.0-only"
|
||||
rust-version = "1.88"
|
||||
@ -67,6 +68,7 @@ libsignal-message-backup = { path = "rust/message-backup" }
|
||||
libsignal-net = { path = "rust/net" }
|
||||
libsignal-net-chat = { path = "rust/net/chat" }
|
||||
libsignal-net-grpc = { path = "rust/net/grpc" }
|
||||
libsignal-node = { path = "rust/bridge/node" }
|
||||
libsignal-protocol = { path = "rust/protocol" }
|
||||
libsignal-svrb = { path = "rust/svrb" }
|
||||
poksho = { path = "rust/poksho" }
|
||||
@ -150,7 +152,7 @@ indexmap = "2.7.0"
|
||||
intmap = "3.1.2"
|
||||
itertools = "0.14.0"
|
||||
jni = "0.21"
|
||||
libc = "0.2.175"
|
||||
libc = "0.2.186"
|
||||
libcrux-ml-kem = { version = "0.0.8", default-features = false }
|
||||
linkme = "0.3.33"
|
||||
log = "0.4.21"
|
||||
@ -160,6 +162,7 @@ mediasan-common = "0.5.3"
|
||||
minidump = { version = "0.22.1", default-features = false }
|
||||
minidump-processor = { version = "0.22.1", default-features = false }
|
||||
minidump-unwind = { version = "0.22.1", default-features = false }
|
||||
minijinja = "2.19.0"
|
||||
mp4san = "0.5.3"
|
||||
neon = { version = "1.1.0", default-features = false }
|
||||
nonzero_ext = "0.3.0"
|
||||
@ -209,7 +212,7 @@ test-log = "0.2.16"
|
||||
testing_logger = "0.1.1"
|
||||
thiserror = "2.0.11"
|
||||
tls-parser = "0.12.2"
|
||||
tokio = "1.45"
|
||||
tokio = "1.52.2"
|
||||
tokio-socks = "0.5.2"
|
||||
tokio-stream = "0.1.16"
|
||||
tokio-tungstenite = "0.28.0"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'LibSignalClient'
|
||||
s.version = '0.93.2'
|
||||
s.version = '0.94.1'
|
||||
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
|
||||
|
||||
s.homepage = 'https://github.com/signalapp/libsignal'
|
||||
|
||||
@ -214,7 +214,7 @@ $ npm run test
|
||||
|
||||
When testing changes locally, you can use `npm run build` to do an incremental rebuild of the Rust library. Alternately, `npm run build-with-debug-level-logs` will rebuild without filtering out debug- and verbose-level logs.
|
||||
|
||||
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
|
||||
When exposing new APIs to Node, you will need to run `just generate-node` in
|
||||
addition to rebuilding.
|
||||
|
||||
[nvm]: https://github.com/nvm-sh/nvm
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
v0.93.2
|
||||
v0.94.1
|
||||
|
||||
- CDSI: production enclave switch to 15637fa1
|
||||
- SVR: New enclaves for 2026Q2 for staging, and configurations (but not use) for prod.
|
||||
- node: Expose SVR2-related functionality
|
||||
- node: Support non-ASCII usernames and passwords in proxy URLs
|
||||
- Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads.
|
||||
|
||||
- keytrans: Add reset account data field functionality for all platforms.
|
||||
|
||||
@ -3329,7 +3329,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.1
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.3
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Alex Crichton
|
||||
@ -3424,7 +3424,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## mio 1.1.1
|
||||
## mio 1.2.0
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Carl Lerche and other MIO contributors
|
||||
@ -3601,37 +3601,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.180
|
||||
|
||||
```
|
||||
Copyright (c) 2014-2020 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## flate2 1.1.5
|
||||
|
||||
```
|
||||
@ -4151,6 +4120,31 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## httpdate 1.0.3
|
||||
|
||||
```
|
||||
Copyright (c) 2016 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## rustc_version 0.4.1
|
||||
|
||||
```
|
||||
@ -5952,6 +5946,37 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.186
|
||||
|
||||
```
|
||||
Copyright (c) The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## arrayvec 0.7.6
|
||||
|
||||
```
|
||||
@ -6276,7 +6301,7 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-macros 2.6.0
|
||||
## tokio-macros 2.7.0
|
||||
|
||||
```
|
||||
MIT License
|
||||
@ -6544,7 +6569,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
@ -3329,7 +3329,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.1
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, pkg-config 0.3.32, rustc-demangle 0.1.26, socket2 0.6.3
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Alex Crichton
|
||||
@ -3424,7 +3424,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## mio 1.1.1
|
||||
## mio 1.2.0
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Carl Lerche and other MIO contributors
|
||||
@ -3601,37 +3601,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.180
|
||||
|
||||
```
|
||||
Copyright (c) 2014-2020 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## flate2 1.1.5
|
||||
|
||||
```
|
||||
@ -4151,6 +4120,31 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## httpdate 1.0.3
|
||||
|
||||
```
|
||||
Copyright (c) 2016 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## rustc_version 0.4.1
|
||||
|
||||
```
|
||||
@ -5952,6 +5946,37 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.186
|
||||
|
||||
```
|
||||
Copyright (c) The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## arrayvec 0.7.6
|
||||
|
||||
```
|
||||
@ -6276,7 +6301,7 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-macros 2.6.0
|
||||
## tokio-macros 2.7.0
|
||||
|
||||
```
|
||||
MIT License
|
||||
@ -6544,7 +6569,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
@ -3209,7 +3209,7 @@ third-party/chromium/LICENSE.
|
||||
|
||||
```
|
||||
|
||||
## windows-core 0.62.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.60.2, windows-sys 0.61.2, windows-targets 0.52.6, windows-targets 0.53.5, windows_aarch64_msvc 0.52.6, windows_aarch64_msvc 0.53.1, windows_x86_64_gnu 0.52.6, windows_x86_64_gnu 0.53.1, windows_x86_64_msvc 0.52.6, windows_x86_64_msvc 0.53.1
|
||||
## windows-core 0.62.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.2.1, windows-result 0.4.1, windows-strings 0.5.1, windows-sys 0.52.0, windows-sys 0.59.0, windows-sys 0.61.2, windows-targets 0.52.6, windows_aarch64_msvc 0.52.6, windows_x86_64_gnu 0.52.6, windows_x86_64_msvc 0.52.6
|
||||
|
||||
```
|
||||
MIT License
|
||||
@ -3419,7 +3419,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, openssl-probe 0.2.0, rustc-demangle 0.1.26, socket2 0.6.1
|
||||
## backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, openssl-probe 0.2.0, rustc-demangle 0.1.26, socket2 0.6.3
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Alex Crichton
|
||||
@ -3482,7 +3482,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## mio 1.1.1
|
||||
## mio 1.2.0
|
||||
|
||||
```
|
||||
Copyright (c) 2014 Carl Lerche and other MIO contributors
|
||||
@ -3659,37 +3659,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.180
|
||||
|
||||
```
|
||||
Copyright (c) 2014-2020 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## flate2 1.1.5
|
||||
|
||||
```
|
||||
@ -4272,6 +4241,31 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## httpdate 1.0.3
|
||||
|
||||
```
|
||||
Copyright (c) 2016 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## rustc_version 0.4.1
|
||||
|
||||
```
|
||||
@ -6199,6 +6193,37 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## libc 0.2.186
|
||||
|
||||
```
|
||||
Copyright (c) The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## arrayvec 0.7.6
|
||||
|
||||
```
|
||||
@ -6635,7 +6660,7 @@ SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-macros 2.6.0
|
||||
## tokio-macros 2.7.0
|
||||
|
||||
```
|
||||
MIT License
|
||||
@ -6930,7 +6955,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
```
|
||||
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0
|
||||
## tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
@ -3517,7 +3517,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, rustc-demangle 0.1.26, socket2 0.6.1</string>
|
||||
<string>backtrace 0.3.76, cc 1.2.52, cfg-if 1.0.4, cmake 0.1.57, find-msvc-tools 0.1.7, rustc-demangle 0.1.26, socket2 0.6.3</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
@ -3582,7 +3582,7 @@ THE SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>mio 1.1.1</string>
|
||||
<string>mio 1.2.0</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
@ -3758,41 +3758,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2014-2020 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>libc 0.2.180</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2014-2025 Alex Crichton
|
||||
@ -4355,6 +4320,35 @@ DEALINGS IN THE SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2016 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>httpdate 1.0.3</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2016 The Rust Project Developers
|
||||
@ -6356,6 +6350,41 @@ SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>libc 0.2.186</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) Ulrik Sverdrup "bluss" 2015-2023
|
||||
@ -6756,7 +6785,7 @@ SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>tokio-macros 2.6.0</string>
|
||||
<string>tokio-macros 2.7.0</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
@ -7063,7 +7092,7 @@ SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT License</string>
|
||||
<key>Title</key>
|
||||
<string>tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.49.0</string>
|
||||
<string>tokio-stream 0.1.18, tokio-util 0.7.18, tokio 1.52.2</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
<li><a href="#MIT">MIT License</a> (354)</li>
|
||||
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (36)</li>
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (25)</li>
|
||||
<li><a href="#MIT">MIT License</a> (349)</li>
|
||||
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (37)</li>
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (27)</li>
|
||||
<li><a href="#BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</a> (9)</li>
|
||||
<li><a href="#ISC">ISC License</a> (4)</li>
|
||||
<li><a href="#MPL-2.0">Mozilla Public License 2.0</a> (2)</li>
|
||||
@ -741,6 +741,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<li><a href="https://crates.io/crates/libsignal-node">libsignal-node</a></li>
|
||||
<li><a href="https://crates.io/crates/signal-neon-futures">signal-neon-futures</a></li>
|
||||
<li><a href="https://crates.io/crates/signal-neon-futures-tests">signal-neon-futures-tests</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-node-native_ts">libsignal-node-native_ts</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge">libsignal-bridge</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge-macros">libsignal-bridge-macros</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge-testing">libsignal-bridge-testing</a></li>
|
||||
@ -2937,6 +2938,8 @@ END OF TERMS AND CONDITIONS
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/getsentry/rust-debugid">debugid 0.8.0</a></li>
|
||||
<li><a href="https://github.com/mitsuhiko/memo-map">memo-map 0.3.3</a></li>
|
||||
<li><a href="https://github.com/mitsuhiko/minijinja">minijinja 2.19.0</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-build 0.14.1</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-derive 0.14.1</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-types 0.14.1</a></li>
|
||||
@ -3558,19 +3561,14 @@ third-party/chromium/LICENSE.
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.45.0</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.52.0</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.59.0</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.60.2</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-sys 0.61.2</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.42.2</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.52.6</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows-targets 0.53.5</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.42.2</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.52.6</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_aarch64_msvc 0.53.1</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_gnu 0.52.6</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_gnu 0.53.1</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.42.2</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.52.6</a></li>
|
||||
<li><a href="https://github.com/microsoft/windows-rs">windows_x86_64_msvc 0.53.1</a></li>
|
||||
</ul>
|
||||
<pre class="license-text"> MIT License
|
||||
|
||||
@ -3839,7 +3837,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
<li><a href="https://github.com/rust-lang/pkg-config-rs">pkg-config 0.3.32</a></li>
|
||||
<li><a href="https://github.com/rust-lang/rustc-demangle">rustc-demangle 0.1.26</a></li>
|
||||
<li><a href="https://github.com/alexcrichton/scoped-tls">scoped-tls 1.0.1</a></li>
|
||||
<li><a href="https://github.com/rust-lang/socket2">socket2 0.6.1</a></li>
|
||||
<li><a href="https://github.com/rust-lang/socket2">socket2 0.6.3</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">Copyright (c) 2014 Alex Crichton
|
||||
|
||||
@ -3940,7 +3938,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/tokio-rs/mio">mio 1.1.1</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/mio">mio 1.2.0</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">Copyright (c) 2014 Carl Lerche and other MIO contributors
|
||||
|
||||
@ -4160,39 +4158,6 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/rust-lang/libc">libc 0.2.180</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">Copyright (c) 2014-2020 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
@ -7207,6 +7172,39 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/rust-lang/libc">libc 0.2.186</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">Copyright (c) The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
@ -7731,7 +7729,7 @@ SOFTWARE.
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio-macros 2.6.0</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio-macros 2.7.0</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">MIT License
|
||||
|
||||
@ -8055,7 +8053,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio-stream 0.1.18</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio-util 0.7.18</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio 1.49.0</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/tokio">tokio 1.52.2</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">MIT License
|
||||
|
||||
|
||||
@ -21,15 +21,14 @@ brew "rustup"
|
||||
brew "shellcheck"
|
||||
brew "swiftlint"
|
||||
brew "taplo"
|
||||
brew "terraform"
|
||||
brew "yamllint"
|
||||
cask "google-cloud-sdk"
|
||||
cask "gcloud-cli"
|
||||
EOF
|
||||
|
||||
# Install Python tools using pipx.
|
||||
# This keeps their dependencies isolated from other things on your system,
|
||||
# but is still global state for each tool. We may some day want to switch this to a venv instead.
|
||||
"$(brew --prefix pipx)/bin/pipx" install mypy
|
||||
"$(brew --prefix pipx)/bin/pipx" install "mypy<2.0"
|
||||
"$(brew --prefix pipx)/bin/pipx" install flake8
|
||||
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
|
||||
flake8-comprehensions \
|
||||
|
||||
@ -23,7 +23,7 @@ repositories {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.93.2"
|
||||
version = "0.94.1"
|
||||
group = "org.signal"
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@ -63,7 +63,7 @@ public class AuthenticatedChatConnection extends ChatConnection {
|
||||
*/
|
||||
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener) {
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0]);
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0], new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,14 +72,20 @@ public class AuthenticatedChatConnection extends ChatConnection {
|
||||
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
|
||||
*/
|
||||
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener, String[] alerts) {
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
String[] grpcOverrides,
|
||||
String[] alerts) {
|
||||
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle -> {
|
||||
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
|
||||
long fakeChatConnection =
|
||||
NativeTesting.TESTING_FakeChatConnection_Create(
|
||||
asyncContextHandle, bridgeListener, String.join("\n", alerts));
|
||||
asyncContextHandle,
|
||||
bridgeListener,
|
||||
String.join("\n", grpcOverrides),
|
||||
String.join("\n", alerts));
|
||||
AuthenticatedChatConnection chat =
|
||||
new AuthenticatedChatConnection(
|
||||
tokioAsyncContext,
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
//
|
||||
package org.signal.libsignal.net
|
||||
|
||||
public abstract class KeyTransparency {
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.keytrans.Store
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public object KeyTransparency {
|
||||
/**
|
||||
* Mode of the key transparency operation.
|
||||
*
|
||||
@ -32,4 +36,44 @@ public abstract class KeyTransparency {
|
||||
|
||||
public fun isSelf(): Boolean = this is Self
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag identifying an optional field of the account data.
|
||||
*
|
||||
* (Must be in sync with the Rust counterpart)
|
||||
*/
|
||||
public enum class AccountDataField(
|
||||
public val value: Int,
|
||||
) {
|
||||
E164(0),
|
||||
USERNAME_HASH(1),
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a particular field in the data associated with given ACI.
|
||||
*
|
||||
* Must only be called for the "self" account when either E.164 or username change is performed.
|
||||
*
|
||||
* Upon successful completion the data associated with the account will be updated in the store, if it
|
||||
* was present to begin with, noop if it was not.
|
||||
*
|
||||
* @param aci An ACI of "self" account.
|
||||
* @param field Account data field to be reset (E.164 or username hash)
|
||||
* @param store local persistent storage for key transparency-related data.
|
||||
* @throws IllegalArgumentException if the stored data cannot be decoded correctly, which means data corruption.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun resetField(
|
||||
aci: ServiceId.Aci,
|
||||
field: AccountDataField,
|
||||
store: Store,
|
||||
) {
|
||||
store.getAccountData(aci).map {
|
||||
val updated = Native.KeyTransparency_ResetDataField(it, field.value)
|
||||
if (updated.isEmpty()) {
|
||||
throw IllegalArgumentException("failed to decode account data")
|
||||
}
|
||||
store.setAccountData(aci, updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,13 +77,26 @@ public class UnauthenticatedChatConnection extends ChatConnection {
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
Network.Environment ktEnvironment) {
|
||||
return fakeConnect(tokioAsyncContext, listener, new String[0], ktEnvironment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only method to create a {@code UnauthenticatedChatConnection} connected to a fake remote.
|
||||
*
|
||||
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
|
||||
*/
|
||||
public static Pair<UnauthenticatedChatConnection, FakeChatRemote> fakeConnect(
|
||||
final TokioAsyncContext tokioAsyncContext,
|
||||
ChatConnectionListener listener,
|
||||
String[] grpcOverrides,
|
||||
Network.Environment ktEnvironment) {
|
||||
|
||||
return tokioAsyncContext.guardedMap(
|
||||
asyncContextHandle -> {
|
||||
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
|
||||
long fakeChatConnection =
|
||||
NativeTesting.TESTING_FakeChatConnection_Create(
|
||||
asyncContextHandle, bridgeListener, "");
|
||||
asyncContextHandle, bridgeListener, String.join("\n", grpcOverrides), "");
|
||||
UnauthenticatedChatConnection chat =
|
||||
new UnauthenticatedChatConnection(
|
||||
tokioAsyncContext,
|
||||
|
||||
@ -18,7 +18,8 @@ public data class UploadForm(
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@CalledFromNative
|
||||
public fun fromNative(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun fromNative(
|
||||
cdn: Int,
|
||||
key: String,
|
||||
headers: Array<*>,
|
||||
|
||||
@ -169,7 +169,6 @@ class AuthMessagesServiceTest {
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
emptyArray(),
|
||||
)
|
||||
|
||||
listOf(
|
||||
@ -199,7 +198,6 @@ class AuthMessagesServiceTest {
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
emptyArray(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
@ -224,7 +222,6 @@ class AuthMessagesServiceTest {
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
emptyArray(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
@ -268,7 +265,6 @@ class AuthMessagesServiceTest {
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
emptyArray(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
@ -310,7 +306,6 @@ class AuthMessagesServiceTest {
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
emptyArray(),
|
||||
)
|
||||
|
||||
val (responseFuture, requestId) = sendTestMessage(chat, syncMessage = false, fakeRemote)
|
||||
|
||||
@ -263,7 +263,7 @@ public class ChatServiceTest {
|
||||
final Listener listener = new Listener();
|
||||
final Pair<AuthenticatedChatConnection, FakeChatRemote> chatAndFakeRemote =
|
||||
AuthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext, listener, new String[] {"UPPERcase", "lowercase"});
|
||||
tokioAsyncContext, listener, new String[0], new String[] {"UPPERcase", "lowercase"});
|
||||
final AuthenticatedChatConnection chat = chatAndFakeRemote.getFirst();
|
||||
final FakeChatRemote fakeRemote = chatAndFakeRemote.getSecond();
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import java.util.UUID;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.keytrans.KeyTransparencyException;
|
||||
import org.signal.libsignal.keytrans.TestStore;
|
||||
import org.signal.libsignal.keytrans.VerificationFailedException;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
@ -60,4 +61,29 @@ public class KeyTransparencyTest {
|
||||
public void canBridgeChatSendError() {
|
||||
assertThrows(TimeoutException.class, NativeTesting::TESTING_KeyTransChatSendError);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldThrowsOnCorruptData() {
|
||||
var store = new TestStore();
|
||||
store.setAccountData(TEST_ACI, new byte[] {1, 2, 3});
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldIsNoopWhenDataIsMissing() {
|
||||
var store = new TestStore();
|
||||
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
|
||||
assert (store.storage.get(TEST_ACI).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetFieldUpdatesStoreOnSuccess() {
|
||||
var store = new TestStore();
|
||||
store.setAccountData(TEST_ACI, NativeTesting.TESTING_KeyTransStoredAccountData());
|
||||
assertEquals(1, store.storage.get(TEST_ACI).size());
|
||||
KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store);
|
||||
assertEquals(2, store.storage.get(TEST_ACI).size());
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,8 +300,8 @@ public class SessionBuilderTest {
|
||||
aliceSessionBuilder.process(bobPreKey, Instant.EPOCH);
|
||||
|
||||
SessionRecord initialSession = aliceStore.loadSession(BOB_ADDRESS);
|
||||
assertTrue(initialSession.hasSenderChain(Instant.EPOCH));
|
||||
assertFalse(initialSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
|
||||
assertTrue(initialSession.hasSenderChain(1.0, Instant.EPOCH));
|
||||
assertFalse(initialSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
|
||||
|
||||
String originalMessage = "Good, fast, cheap: pick two";
|
||||
SessionCipher aliceSessionCipher = new SessionCipher(aliceStore, ALICE_ADDRESS, BOB_ADDRESS);
|
||||
@ -311,8 +311,8 @@ public class SessionBuilderTest {
|
||||
assertTrue(outgoingMessage.getType() == CiphertextMessage.PREKEY_TYPE);
|
||||
|
||||
SessionRecord updatedSession = aliceStore.loadSession(BOB_ADDRESS);
|
||||
assertTrue(updatedSession.hasSenderChain(Instant.EPOCH));
|
||||
assertFalse(updatedSession.hasSenderChain(Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
|
||||
assertTrue(updatedSession.hasSenderChain(1.0, Instant.EPOCH));
|
||||
assertFalse(updatedSession.hasSenderChain(1.0, Instant.EPOCH.plus(90, ChronoUnit.DAYS)));
|
||||
|
||||
try {
|
||||
aliceSessionCipher.encrypt(
|
||||
|
||||
@ -8,8 +8,10 @@ package org.signal.libsignal.protocol;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.internal.NativeTesting;
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||
@ -29,7 +31,7 @@ public class SessionRecordTest {
|
||||
public void testUninitAccess() {
|
||||
SessionRecord empty_record = new SessionRecord();
|
||||
|
||||
assertFalse(empty_record.hasSenderChain());
|
||||
assertFalse(empty_record.hasSenderChain(1.0));
|
||||
|
||||
assertEquals(empty_record.getSessionVersion(), 0);
|
||||
}
|
||||
@ -76,4 +78,25 @@ public class SessionRecordTest {
|
||||
assertThrows(InvalidKeyException.class, () -> record.getKeyPair());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasUsablePQRatio() throws Exception {
|
||||
// Record with key "\x7f\x7f\x7f\x7f....", so it's around a ratio of 0.5
|
||||
SessionRecord recordNoPqRatchet =
|
||||
new SessionRecord(
|
||||
Hex.fromStringCondensedAssert(
|
||||
"0a29080332006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
|
||||
assertTrue(recordNoPqRatchet.hasSenderChain(0.0, Instant.EPOCH));
|
||||
assertTrue(recordNoPqRatchet.hasSenderChain(0.25, Instant.EPOCH));
|
||||
assertFalse(recordNoPqRatchet.hasSenderChain(0.75, Instant.EPOCH));
|
||||
assertFalse(recordNoPqRatchet.hasSenderChain(1.0, Instant.EPOCH));
|
||||
SessionRecord recordWithPq =
|
||||
new SessionRecord(
|
||||
Hex.fromStringCondensedAssert(
|
||||
"0a29080432006a207f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7a0101"));
|
||||
assertTrue(recordWithPq.hasSenderChain(0.0, Instant.EPOCH));
|
||||
assertTrue(recordWithPq.hasSenderChain(0.25, Instant.EPOCH));
|
||||
assertTrue(recordWithPq.hasSenderChain(0.75, Instant.EPOCH));
|
||||
assertTrue(recordWithPq.hasSenderChain(1.0, Instant.EPOCH));
|
||||
}
|
||||
}
|
||||
|
||||
@ -898,5 +898,13 @@
|
||||
{
|
||||
"version": "v0.93.1",
|
||||
"size": 7515792
|
||||
},
|
||||
{
|
||||
"version": "v0.93.2",
|
||||
"size": 7515184
|
||||
},
|
||||
{
|
||||
"version": "v0.94.0",
|
||||
"size": 7522920
|
||||
}
|
||||
]
|
||||
@ -217,6 +217,8 @@ internal object Native {
|
||||
@JvmStatic
|
||||
public external fun AuthenticatedChatConnection_send_message_java(asyncRuntime: ObjectHandle, chat: ObjectHandle, destination: ByteArray, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<Object>, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Void?>
|
||||
@JvmStatic
|
||||
public external fun AuthenticatedChatConnection_send_raw_grpc(asyncRuntime: ObjectHandle, chat: ObjectHandle, service: String, method: String, payload: ByteArray): CompletableFuture<ByteArray>
|
||||
@JvmStatic
|
||||
public external fun AuthenticatedChatConnection_send_sync_message_java(asyncRuntime: ObjectHandle, chat: ObjectHandle, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<Object>, isUrgent: Boolean): CompletableFuture<Void?>
|
||||
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
@ -627,6 +629,8 @@ internal object Native {
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_E164SearchKey(e164: String): ByteArray
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_ResetDataField(accountData: ByteArray, field: Int): ByteArray
|
||||
@JvmStatic
|
||||
public external fun KeyTransparency_UsernameHashSearchKey(hash: ByteArray): ByteArray
|
||||
|
||||
@JvmStatic
|
||||
@ -1207,7 +1211,7 @@ internal object Native {
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun SessionRecord_GetSessionVersion(s: ObjectHandle): Int
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, now: Long): Boolean
|
||||
public external fun SessionRecord_HasUsableSenderChain(s: ObjectHandle, requirePqRatio: Double, now: Long): Boolean
|
||||
@JvmStatic
|
||||
public external fun SessionRecord_NewFresh(): ObjectHandle
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
@ -1245,8 +1249,6 @@ internal object Native {
|
||||
public external fun SignalMessage_GetSerialized(obj: ObjectHandle): ByteArray
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun SignalMessage_New(messageVersion: Int, macKey: ByteArray, senderRatchetKey: ObjectHandle, counter: Int, previousCounter: Int, ciphertext: ByteArray, senderIdentityKey: ObjectHandle, receiverIdentityKey: ObjectHandle, pqRatchet: ByteArray): ObjectHandle
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun SignalMessage_VerifyMac(msg: ObjectHandle, senderIdentityKey: ObjectHandle, receiverIdentityKey: ObjectHandle, macKey: ByteArray): Boolean
|
||||
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun SignedPreKeyRecord_Deserialize(data: ByteArray): ObjectHandle
|
||||
@ -1309,6 +1311,8 @@ internal object Native {
|
||||
public external fun UnauthenticatedChatConnection_send_message(asyncRuntime: ObjectHandle, chat: ObjectHandle, destination: ByteArray, timestamp: Long, deviceIds: IntArray, registrationIds: IntArray, contents: Array<ByteArray>, authKind: Int, authBuffer: ByteArray?, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Void?>
|
||||
@JvmStatic
|
||||
public external fun UnauthenticatedChatConnection_send_multi_recipient_message(asyncRuntime: ObjectHandle, chat: ObjectHandle, payload: ByteArray, timestamp: Long, auth: ByteArray?, onlineOnly: Boolean, isUrgent: Boolean): CompletableFuture<Array<Object>>
|
||||
@JvmStatic
|
||||
public external fun UnauthenticatedChatConnection_send_raw_grpc(asyncRuntime: ObjectHandle, chat: ObjectHandle, service: String, method: String, payload: ByteArray): CompletableFuture<ByteArray>
|
||||
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun UnidentifiedSenderMessageContent_Deserialize(data: ByteArray): ObjectHandle
|
||||
|
||||
@ -116,7 +116,7 @@ public object NativeTesting {
|
||||
@JvmStatic
|
||||
public external fun TESTING_ErrorOnReturnSync(needsCleanup: Object): Object
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatConnection_Create(tokio: ObjectHandle, listener: BridgeChatListener, alertsJoinedByNewlines: String): ObjectHandle
|
||||
public external fun TESTING_FakeChatConnection_Create(tokio: ObjectHandle, listener: BridgeChatListener, grpcOverridesJoinedByNewlines: String, alertsJoinedByNewlines: String): ObjectHandle
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatConnection_CreateProvisioning(tokio: ObjectHandle, listener: BridgeProvisioningListener): ObjectHandle
|
||||
@JvmStatic
|
||||
@ -128,14 +128,26 @@ public object NativeTesting {
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatConnection_TakeUnauthenticatedChat(chat: ObjectHandle): ObjectHandle
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_BinprotoToJson(name: String, input: ByteArray): String
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_GrpcFrameForMessageLength(len: Int): ByteArray
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(chat: ObjectHandle): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_JsonToBinproto(name: String, input: String): ByteArray
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_NextGrpcMessage(input: ByteArray, offset: Int): Pair<Int, Int>
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_ReceiveIncomingGrpcRequest(asyncRuntime: ObjectHandle, chat: ObjectHandle): CompletableFuture<Pair<ObjectHandle, Long>?>
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_ReceiveIncomingRequest(asyncRuntime: ObjectHandle, chat: ObjectHandle): CompletableFuture<Pair<ObjectHandle, Long>?>
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_SendRawServerRequest(chat: ObjectHandle, bytes: ByteArray): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_SendRawServerResponse(chat: ObjectHandle, bytes: ByteArray): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_SendServerGrpcResponse(asyncRuntime: ObjectHandle, chat: ObjectHandle, response: ObjectHandle): CompletableFuture<Void?>
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatRemoteEnd_SendServerResponse(chat: ObjectHandle, response: ObjectHandle): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_FakeChatResponse_Create(id: Long, status: Int, message: String, headers: Array<Object>, body: ByteArray?): ObjectHandle
|
||||
@ -174,6 +186,8 @@ public object NativeTesting {
|
||||
@JvmStatic @Throws(Exception::class)
|
||||
public external fun TESTING_KeyTransNonFatalVerificationFailure(): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_KeyTransStoredAccountData(): ByteArray
|
||||
@JvmStatic
|
||||
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_Destroy(handle: ObjectHandle): Unit
|
||||
@JvmStatic
|
||||
public external fun TESTING_NonSuspendingBackgroundThreadRuntime_New(): ObjectHandle
|
||||
|
||||
@ -7,11 +7,9 @@ package org.signal.libsignal.protocol.message;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.signal.libsignal.internal.CalledFromNative;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.InvalidVersionException;
|
||||
@ -66,28 +64,6 @@ public class SignalMessage extends NativeHandleGuard.SimpleOwner
|
||||
return filterExceptions(() -> guardedMapChecked(Native::SignalMessage_GetPqRatchet));
|
||||
}
|
||||
|
||||
public void verifyMac(
|
||||
IdentityKey senderIdentityKey, IdentityKey receiverIdentityKey, SecretKeySpec macKey)
|
||||
throws InvalidMessageException, InvalidKeyException {
|
||||
try (NativeHandleGuard guard = new NativeHandleGuard(this);
|
||||
NativeHandleGuard senderIdentityGuard =
|
||||
new NativeHandleGuard(senderIdentityKey.getPublicKey());
|
||||
NativeHandleGuard receiverIdentityGuard =
|
||||
new NativeHandleGuard(receiverIdentityKey.getPublicKey()); ) {
|
||||
if (!filterExceptions(
|
||||
InvalidMessageException.class,
|
||||
InvalidKeyException.class,
|
||||
() ->
|
||||
Native.SignalMessage_VerifyMac(
|
||||
guard.nativeHandle(),
|
||||
senderIdentityGuard.nativeHandle(),
|
||||
receiverIdentityGuard.nativeHandle(),
|
||||
macKey.getEncoded()))) {
|
||||
throw new InvalidMessageException("Bad Mac!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return filterExceptions(() -> guardedMapChecked(Native::SignalMessage_GetSerialized));
|
||||
|
||||
@ -98,8 +98,8 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
|
||||
*
|
||||
* <p>If there is no current session, returns {@code false}.
|
||||
*/
|
||||
public boolean hasSenderChain() {
|
||||
return hasSenderChain(Instant.now());
|
||||
public boolean hasSenderChain(double requirePqRatio) {
|
||||
return hasSenderChain(requirePqRatio, Instant.now());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,12 +109,13 @@ public class SessionRecord extends NativeHandleGuard.SimpleOwner {
|
||||
*
|
||||
* <p>You should only use this overload if you need to test session expiration.
|
||||
*/
|
||||
public boolean hasSenderChain(Instant now) {
|
||||
public boolean hasSenderChain(double requirePqRatio, Instant now) {
|
||||
return filterExceptions(
|
||||
() ->
|
||||
guardedMapChecked(
|
||||
(nativeHandle) ->
|
||||
Native.SessionRecord_HasUsableSenderChain(nativeHandle, now.toEpochMilli())));
|
||||
Native.SessionRecord_HasUsableSenderChain(
|
||||
nativeHandle, requirePqRatio, now.toEpochMilli())));
|
||||
}
|
||||
|
||||
public boolean currentRatchetKeyMatches(ECPublicKey key) {
|
||||
|
||||
2
justfile
2
justfile
@ -14,7 +14,7 @@ generate-ffi:
|
||||
swift/build_ffi.sh --generate-ffi
|
||||
|
||||
generate-node:
|
||||
rust/bridge/node/bin/gen_ts_decl.py
|
||||
cargo run -p libsignal-node-native_ts
|
||||
|
||||
alias generate-java := generate-jni
|
||||
alias generate-swift := generate-ffi
|
||||
|
||||
4
node/package-lock.json
generated
4
node/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@signalapp/libsignal-client",
|
||||
"version": "0.93.2",
|
||||
"version": "0.94.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@signalapp/libsignal-client",
|
||||
"version": "0.93.2",
|
||||
"version": "0.94.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@signalapp/libsignal-client",
|
||||
"version": "0.93.2",
|
||||
"version": "0.94.1",
|
||||
"repository": "github:signalapp/libsignal",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
@ -24,8 +24,8 @@
|
||||
"build": "python3 build_node_bridge.py",
|
||||
"build-with-debug-level-logs": "python3 build_node_bridge.py --debug-level-logs",
|
||||
"clean": "rimraf dist build prebuilds",
|
||||
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
|
||||
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
|
||||
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
|
||||
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
|
||||
"install": "echo Use \\`npm run build\\` to build the native library if needed",
|
||||
"lint": "eslint .",
|
||||
"prepack": "cp ../acknowledgments/acknowledgments-desktop.md dist/acknowledgments.md",
|
||||
|
||||
3414
node/ts/Native.ts
3414
node/ts/Native.ts
File diff suppressed because it is too large
Load Diff
@ -411,19 +411,6 @@ export class SignalMessage {
|
||||
serialize(): Uint8Array<ArrayBuffer> {
|
||||
return Native.SignalMessage_GetSerialized(this);
|
||||
}
|
||||
|
||||
verifyMac(
|
||||
senderIdentityKey: PublicKey,
|
||||
recevierIdentityKey: PublicKey,
|
||||
macKey: Uint8Array<ArrayBuffer>
|
||||
): boolean {
|
||||
return Native.SignalMessage_VerifyMac(
|
||||
this,
|
||||
senderIdentityKey,
|
||||
recevierIdentityKey,
|
||||
macKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PreKeySignalMessage {
|
||||
@ -518,8 +505,12 @@ export class SessionRecord {
|
||||
*
|
||||
* If there is no current session, returns false.
|
||||
*/
|
||||
hasCurrentState(now: Date = new Date()): boolean {
|
||||
return Native.SessionRecord_HasUsableSenderChain(this, now.getTime());
|
||||
hasCurrentState(requirePqRatio: number, now: Date = new Date()): boolean {
|
||||
return Native.SessionRecord_HasUsableSenderChain(
|
||||
this,
|
||||
requirePqRatio,
|
||||
now.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
currentRatchetKeyMatches(key: PublicKey): boolean {
|
||||
|
||||
@ -203,12 +203,15 @@ export class UnauthenticatedChatConnection implements ChatConnection {
|
||||
*
|
||||
* @param asyncContext the async runtime to use
|
||||
* @param listener the listener to send events to
|
||||
* @param grpcOverrides gRPC method names to prefer for typed APIs that have both WS and gRPC
|
||||
* implementations.
|
||||
* @returns an {@link UnauthenticatedChatConnection} and handle for the remote
|
||||
* end of the fake connection.
|
||||
*/
|
||||
public static fakeConnect(
|
||||
asyncContext: TokioAsyncContext,
|
||||
listener: ChatServiceListener
|
||||
listener: ChatServiceListener,
|
||||
grpcOverrides?: ReadonlyArray<string>
|
||||
): [UnauthenticatedChatConnection, FakeChatRemote] {
|
||||
const nativeChatListener = makeNativeChatListener(asyncContext, listener);
|
||||
|
||||
@ -216,6 +219,7 @@ export class UnauthenticatedChatConnection implements ChatConnection {
|
||||
Native.TESTING_FakeChatConnection_Create(
|
||||
asyncContext,
|
||||
new WeakListenerWrapper(nativeChatListener),
|
||||
grpcOverrides?.join('\n') ?? '',
|
||||
''
|
||||
)
|
||||
);
|
||||
@ -319,13 +323,16 @@ export class AuthenticatedChatConnection implements ChatConnection {
|
||||
*
|
||||
* @param asyncContext the async runtime to use
|
||||
* @param listener the listener to send events to
|
||||
* @param grpcOverrides gRPC method names to prefer for typed APIs that have both WS and gRPC
|
||||
* implementations.
|
||||
* @param alerts alerts to send immediately upon connect
|
||||
* @returns an {@link AuthenticatedChatConnection} and handle for the remote
|
||||
* end of the fake connection.
|
||||
* @returns an {@link AuthenticatedChatConnection} and handle for the remote end of the fake
|
||||
* connection.
|
||||
*/
|
||||
public static fakeConnect(
|
||||
asyncContext: TokioAsyncContext,
|
||||
listener: ChatServiceListener,
|
||||
grpcOverrides?: ReadonlyArray<string>,
|
||||
alerts?: ReadonlyArray<string>
|
||||
): [AuthenticatedChatConnection, FakeChatRemote] {
|
||||
const nativeChatListener = makeNativeChatListener(asyncContext, listener);
|
||||
@ -334,6 +341,7 @@ export class AuthenticatedChatConnection implements ChatConnection {
|
||||
Native.TESTING_FakeChatConnection_Create(
|
||||
asyncContext,
|
||||
new WeakListenerWrapper(nativeChatListener),
|
||||
grpcOverrides?.join('\n') ?? '',
|
||||
alerts?.join('\n') ?? ''
|
||||
)
|
||||
);
|
||||
|
||||
@ -168,6 +168,46 @@ export interface Client {
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tag identifying an optional field of the account data.
|
||||
*
|
||||
* (Must be in sync with the Rust counterpart)
|
||||
*/
|
||||
export enum AccountDataField {
|
||||
E164 = 0,
|
||||
UsernameHash = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a particular field in the data associated with given ACI.
|
||||
*
|
||||
* Must only be called for the "self" account when either E.164 or username
|
||||
* change is performed.
|
||||
*
|
||||
* Upon successful completion the data associated with the account will be
|
||||
* updated in the store, if it was present to begin with, noop if it was not.
|
||||
*
|
||||
* @param aci - An ACI of "self" account.
|
||||
* @param field - Account data field to be reset (E.164 or username hash).
|
||||
* @param store - local persistent storage for key transparency-related data.
|
||||
* @throws {TypeError} if the stored data cannot be decoded correctly, which means data corruption.
|
||||
*/
|
||||
export async function resetField(
|
||||
aci: Aci,
|
||||
field: AccountDataField,
|
||||
store: Store
|
||||
): Promise<void> {
|
||||
const accountData = await store.getAccountData(aci);
|
||||
if (accountData === null) {
|
||||
return;
|
||||
}
|
||||
const updated = Native.KeyTransparency_ResetDataField(accountData, field);
|
||||
if (updated.length === 0) {
|
||||
throw new TypeError('failed to decode account data');
|
||||
}
|
||||
await store.setAccountData(aci, updated);
|
||||
}
|
||||
|
||||
export class ClientImpl implements Client {
|
||||
constructor(
|
||||
private readonly asyncContext: TokioAsyncContext,
|
||||
|
||||
@ -90,6 +90,33 @@ describe('KeyTransparency bridging', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeyTransparency.resetField', () => {
|
||||
it('throws on corrupt data', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await store.setAccountData(testAci, new Uint8Array([1, 2, 3]));
|
||||
await expect(
|
||||
KT.resetField(testAci, KT.AccountDataField.E164, store)
|
||||
).to.be.rejectedWith(TypeError);
|
||||
});
|
||||
|
||||
it('is a noop when data is missing', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await KT.resetField(testAci, KT.AccountDataField.E164, store);
|
||||
expect(store.storage.get(testAci)).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('updates store on success', async () => {
|
||||
const store = new InMemoryKtStore();
|
||||
await store.setAccountData(
|
||||
testAci,
|
||||
Native.TESTING_KeyTransStoredAccountData()
|
||||
);
|
||||
expect(store.storage.get(testAci)).to.have.lengthOf(1);
|
||||
await KT.resetField(testAci, KT.AccountDataField.E164, store);
|
||||
expect(store.storage.get(testAci)).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeyTransparency network errors', () => {
|
||||
it('can bridge network errors', async () => {
|
||||
async function run(statusCode: number, headers: string[] = []) {
|
||||
|
||||
@ -529,6 +529,7 @@ describe('chat service api', () => {
|
||||
const [_chat, fakeRemote] = AuthenticatedChatConnection.fakeConnect(
|
||||
tokio,
|
||||
listener,
|
||||
[],
|
||||
['UPPERcase', 'lowercase']
|
||||
);
|
||||
|
||||
|
||||
@ -812,7 +812,7 @@ for (const testCase of sessionVersionTestCases) {
|
||||
assert(session.serialize().length > 0);
|
||||
assert.deepEqual(session.localRegistrationId(), 5);
|
||||
assert.deepEqual(session.remoteRegistrationId(), 5);
|
||||
assert(session.hasCurrentState());
|
||||
assert(session.hasCurrentState(1.0));
|
||||
assert(
|
||||
!session.currentRatchetKeyMatches(
|
||||
SignalClient.PrivateKey.generate().getPublicKey()
|
||||
@ -820,7 +820,7 @@ for (const testCase of sessionVersionTestCases) {
|
||||
);
|
||||
|
||||
session.archiveCurrentState();
|
||||
assert(!session.hasCurrentState());
|
||||
assert(!session.hasCurrentState(1.0));
|
||||
assert(
|
||||
!session.currentRatchetKeyMatches(
|
||||
SignalClient.PrivateKey.generate().getPublicKey()
|
||||
@ -968,8 +968,12 @@ for (const testCase of sessionVersionTestCases) {
|
||||
);
|
||||
|
||||
const initialSession = await aliceStores.session.getSession(bAddress);
|
||||
assert.isTrue(initialSession?.hasCurrentState(new Date('2020-01-01')));
|
||||
assert.isFalse(initialSession?.hasCurrentState(new Date('2023-01-01')));
|
||||
assert.isTrue(
|
||||
initialSession?.hasCurrentState(1.0, new Date('2020-01-01'))
|
||||
);
|
||||
assert.isFalse(
|
||||
initialSession?.hasCurrentState(1.0, new Date('2023-01-01'))
|
||||
);
|
||||
|
||||
const aMessage = Buffer.from('Greetings hoo-man', 'utf8');
|
||||
const aCiphertext = await SignalClient.signalEncrypt(
|
||||
@ -987,8 +991,12 @@ for (const testCase of sessionVersionTestCases) {
|
||||
);
|
||||
|
||||
const updatedSession = await aliceStores.session.getSession(bAddress);
|
||||
assert.isTrue(updatedSession?.hasCurrentState(new Date('2020-01-01')));
|
||||
assert.isFalse(updatedSession?.hasCurrentState(new Date('2023-01-01')));
|
||||
assert.isTrue(
|
||||
updatedSession?.hasCurrentState(1.0, new Date('2020-01-01'))
|
||||
);
|
||||
assert.isFalse(
|
||||
updatedSession?.hasCurrentState(1.0, new Date('2023-01-01'))
|
||||
);
|
||||
|
||||
await assert.isRejected(
|
||||
SignalClient.signalEncrypt(
|
||||
|
||||
@ -80,8 +80,8 @@ class definition. For Swift, the `cbindgen` output is saved directly to a
|
||||
C-style `.h` file that the Swift toolchain can consume.
|
||||
|
||||
For TypeScript, the [`libsignal-node`] crate is expanded and processed by
|
||||
[`gen_ts_decl.py`](./node/bin/gen_ts_decl.py) and the output is interpolated into
|
||||
[`Native.ts.in`](./node/bin/Native.ts.in). The output, however, only
|
||||
[`libsignal-node-native_ts`](./node/native_ts/src/main.rs) and the output is interpolated into
|
||||
[`Native.ts.in`](./node/native_ts/src/Native.ts.in). The output, however, only
|
||||
declares the function signatures; to make them accessible to the JavaScript
|
||||
runtime, additional machinery is used. This takes the form of `#[linkme]`
|
||||
annotations on to the generated entry points; the [`linkme`] crate is used to
|
||||
|
||||
@ -43,6 +43,7 @@ exclude = [
|
||||
"CPromisebool",
|
||||
"CPromiseFfiCdsiLookupResponse",
|
||||
"CPromiseMutPointerRegistrationService",
|
||||
"CPromiseOwnedBufferOfc_uchar",
|
||||
"FfiCdsiLookupResponse",
|
||||
"FfiCdsiLookupResponseEntry",
|
||||
"FfiChatListenerStruct",
|
||||
|
||||
@ -82,6 +82,7 @@ def translate_to_java(typ: str) -> Tuple[str, bool]:
|
||||
'Nullable<ObjectHandle>': 'ObjectHandle',
|
||||
'jint': 'Int',
|
||||
'jlong': 'Long',
|
||||
'jdouble': 'Double',
|
||||
'jboolean': 'Boolean',
|
||||
'JObject': 'Object',
|
||||
'JClass': 'Class<*>',
|
||||
|
||||
@ -15,17 +15,23 @@ workspace = true
|
||||
|
||||
[lib]
|
||||
name = "signal_node"
|
||||
crate-type = ["cdylib"]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
# Here for bridge_fn uniformity
|
||||
node = []
|
||||
default = ["node"]
|
||||
metadata = [
|
||||
"libsignal-bridge/metadata",
|
||||
"libsignal-bridge-testing/metadata",
|
||||
"libsignal-bridge-types/metadata",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
libsignal-bridge = { workspace = true, features = ["node", "signal-media"] }
|
||||
libsignal-bridge-macros = { workspace = true }
|
||||
libsignal-bridge-testing = { workspace = true, features = ["node", "signal-media"] }
|
||||
libsignal-bridge-types = { workspace = true, features = ["node"] }
|
||||
libsignal-protocol = { workspace = true }
|
||||
|
||||
futures = { workspace = true }
|
||||
|
||||
@ -1,357 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2021 Signal Messenger, LLC.
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
import collections
|
||||
import difflib
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Iterable, Iterator, Tuple
|
||||
|
||||
Args = collections.namedtuple('Args', ['verify'])
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
def print_usage_and_exit() -> None:
|
||||
print('usage: %s [--verify]' % sys.argv[0], file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# If the command-line handling below gets any more complicated, this should be switched to argparse.
|
||||
mode = None
|
||||
if len(sys.argv) > 2:
|
||||
print_usage_and_exit()
|
||||
elif len(sys.argv) == 2:
|
||||
mode = sys.argv[1]
|
||||
if mode != '--verify':
|
||||
print_usage_and_exit()
|
||||
|
||||
return Args(verify=mode is not None)
|
||||
|
||||
|
||||
def split_rust_args(args: str) -> Iterator[Tuple[str, str]]:
|
||||
"""
|
||||
Split Rust `arg: Type` pairs separated by commas.
|
||||
|
||||
Account for templates, tuples, and slices.
|
||||
"""
|
||||
while ': ' in args:
|
||||
(name, args) = args.split(': ', maxsplit=1)
|
||||
if name.startswith('mut '):
|
||||
name = name[4:]
|
||||
open_pairs = 0
|
||||
for (i, c) in enumerate(args):
|
||||
if c == ',' and open_pairs == 0:
|
||||
ty = args[:i]
|
||||
args = args[i + 1:]
|
||||
yield (name.strip(), ty.strip())
|
||||
break
|
||||
elif c in ['<', '(', '[']:
|
||||
open_pairs += 1
|
||||
elif c in ['>', ')', ']']:
|
||||
open_pairs -= 1
|
||||
else:
|
||||
yield (name.strip(), args.strip())
|
||||
|
||||
|
||||
def translate_to_ts(typ: str) -> str:
|
||||
typ = typ.replace(' ', '')
|
||||
|
||||
type_map = {
|
||||
'()': 'void',
|
||||
'&[u8]': 'Uint8Array<ArrayBuffer>',
|
||||
'i32': 'number',
|
||||
'u8': 'number',
|
||||
'u16': 'number',
|
||||
'u32': 'number',
|
||||
'u64': 'bigint',
|
||||
'bool': 'boolean',
|
||||
'String': 'string',
|
||||
'&str': 'string',
|
||||
'Vec<u8>': 'Uint8Array<ArrayBuffer>',
|
||||
'Box<[u8]>': 'Uint8Array<ArrayBuffer>',
|
||||
'Box<[u32]>': 'Uint32Array<ArrayBuffer>',
|
||||
'bytes::Bytes': 'Uint8Array<ArrayBuffer>',
|
||||
'ServiceId': 'Uint8Array<ArrayBuffer>',
|
||||
'Aci': 'Uint8Array<ArrayBuffer>',
|
||||
'Pni': 'Uint8Array<ArrayBuffer>',
|
||||
'E164': 'string',
|
||||
"ServiceIdSequence<'_>": 'Uint8Array<ArrayBuffer>',
|
||||
'PathAndQuery': 'string',
|
||||
'LanguageList': 'string[]',
|
||||
'GroupSendFullToken': 'Uint8Array<ArrayBuffer>',
|
||||
'DeviceSpecifier': 'number',
|
||||
'&BackupKey': 'Uint8Array<ArrayBuffer>',
|
||||
'MultiRecipientSendAuthorization': 'Uint8Array<ArrayBuffer> | null',
|
||||
'DisconnectCause': 'Error | null',
|
||||
'::zkgroup::backups::BackupAuthCredential': 'Uint8Array<ArrayBuffer>',
|
||||
'::zkgroup::generic_server_params::GenericServerPublicParams': 'Uint8Array<ArrayBuffer>',
|
||||
}
|
||||
|
||||
if typ in type_map:
|
||||
return type_map[typ]
|
||||
|
||||
if typ.startswith('[u8;') or typ.startswith('&[u8;'):
|
||||
return 'Uint8Array<ArrayBuffer>'
|
||||
|
||||
if typ.startswith('&mutdyn'):
|
||||
return typ[7:]
|
||||
|
||||
if typ.startswith('&dyn'):
|
||||
return typ[4:]
|
||||
|
||||
if typ.startswith('&mut'):
|
||||
return 'Wrapper<' + typ[4:] + '>'
|
||||
|
||||
if typ.startswith('&[&'):
|
||||
assert typ.endswith(']')
|
||||
return 'Wrapper<' + translate_to_ts(typ[3:-1]) + '>[]'
|
||||
|
||||
if typ.startswith('Box<['):
|
||||
assert typ.endswith(']>')
|
||||
return translate_to_ts(typ[5:-2]) + '[]'
|
||||
|
||||
if typ.startswith('Box<dyn'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[7:-1])
|
||||
|
||||
if typ.startswith('Vec<'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[4:-1]) + '[]'
|
||||
|
||||
if typ.startswith('&['):
|
||||
assert typ.endswith(']')
|
||||
return 'Wrapper<' + translate_to_ts(typ[2:-1]) + '>[]'
|
||||
|
||||
if typ.startswith('&'):
|
||||
return 'Wrapper<' + typ[1:] + '>'
|
||||
|
||||
if typ.startswith('('):
|
||||
assert typ.endswith(')'), typ
|
||||
inner = typ[1:-1].split(',')
|
||||
if len(inner) == 1:
|
||||
return translate_to_ts(inner[0])
|
||||
return '[' + ', '.join(translate_to_ts(x) for x in inner) + ']'
|
||||
|
||||
if typ.startswith('Option<'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[7:-1]) + ' | null'
|
||||
|
||||
if typ.startswith('Result<'):
|
||||
assert typ.endswith('>')
|
||||
type_args = typ[7:-1]
|
||||
(success_type, *failure_type) = type_args.rsplit(',', 1)
|
||||
if failure_type and ')' in failure_type[0]:
|
||||
success_type = type_args
|
||||
return translate_to_ts(success_type)
|
||||
|
||||
if typ.startswith('std::result::Result<'):
|
||||
assert typ.endswith('>')
|
||||
type_args = typ[20:-1]
|
||||
(success_type, *failure_type) = type_args.rsplit(',', 1)
|
||||
if failure_type and ')' in failure_type[0]:
|
||||
success_type = type_args
|
||||
return translate_to_ts(success_type)
|
||||
|
||||
if typ.startswith('Promise<'):
|
||||
assert typ.endswith('>')
|
||||
return 'Promise<' + translate_to_ts(typ[8:-1]) + '>'
|
||||
|
||||
if typ.startswith('CancellablePromise<'):
|
||||
assert typ.endswith('>')
|
||||
return 'CancellablePromise<' + translate_to_ts(typ[19:-1]) + '>'
|
||||
|
||||
if typ.startswith('AsType<'):
|
||||
assert typ.endswith('>')
|
||||
assert ',' in typ
|
||||
return translate_to_ts(typ.split(',')[1][:-1])
|
||||
|
||||
if typ.startswith('Ignored<'):
|
||||
assert typ.endswith('>')
|
||||
return 'null'
|
||||
|
||||
return typ
|
||||
|
||||
|
||||
DIAGNOSTICS_TO_IGNORE = [
|
||||
r'warning: \d+ warnings? emitted',
|
||||
r'warning: unused import',
|
||||
r'warning: field.+ never read',
|
||||
r'warning: variant.+ never constructed',
|
||||
r'warning: method.+ never used',
|
||||
r'warning: associated function.+ never used',
|
||||
]
|
||||
SHOULD_IGNORE_PATTERN = re.compile('(' + ')|('.join(DIAGNOSTICS_TO_IGNORE) + ')')
|
||||
|
||||
|
||||
def camelcase(arg: str) -> str:
|
||||
return re.sub(
|
||||
# Preserve double-underscores and leading underscores,
|
||||
# but remove single underscores and capitalize the following letter.
|
||||
r'([^_])_([^_])',
|
||||
lambda match: match.group(1) + match.group(2).upper(),
|
||||
arg)
|
||||
|
||||
|
||||
def rewrite_function_as_property(ts_function: str) -> str:
|
||||
return ts_function.replace('(', ': (', 1).replace('):', ') =>')
|
||||
|
||||
|
||||
def rewrite_fn(function_match: re.Match[str]) -> str:
|
||||
(prefix, fn_args, ret_type) = function_match.groups()
|
||||
|
||||
ts_ret_type = translate_to_ts(ret_type)
|
||||
ts_args = []
|
||||
|
||||
for (arg_name, arg_type) in split_rust_args(fn_args):
|
||||
ts_arg_type = translate_to_ts(arg_type)
|
||||
ts_args.append('%s: %s' % (camelcase(arg_name.strip()), ts_arg_type))
|
||||
|
||||
return '%s(%s): %s;' % (prefix, ', '.join(ts_args), ts_ret_type)
|
||||
|
||||
|
||||
def rewrite_trait(decl: str, function_sig: re.Pattern[str]) -> Iterator[str]:
|
||||
for line in decl.split('\\n'):
|
||||
if function_match := function_sig.match(line.rstrip(';')):
|
||||
yield ' ' + rewrite_function_as_property(rewrite_fn(function_match))
|
||||
continue
|
||||
|
||||
# Fix backslash-escaped double-quotes.
|
||||
yield bytes(line, 'utf-8').decode('unicode_escape')
|
||||
|
||||
|
||||
def collect_decls(crate_dir: str, features: Iterable[str] = ()) -> Iterator[str]:
|
||||
args = [
|
||||
'cargo',
|
||||
'rustc',
|
||||
'-q',
|
||||
'--profile=check',
|
||||
'--features', ','.join(features),
|
||||
'--message-format=short',
|
||||
'--color=never',
|
||||
'--',
|
||||
'-Zunpretty=expanded']
|
||||
rustc = subprocess.Popen(args, cwd=crate_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
(raw_stdout, raw_stderr) = rustc.communicate()
|
||||
|
||||
stdout = str(raw_stdout.decode('utf8'))
|
||||
stderr = str(raw_stderr.decode('utf8'))
|
||||
|
||||
had_error = False
|
||||
for l in stderr.split('\n'):
|
||||
if l == '':
|
||||
continue
|
||||
|
||||
if SHOULD_IGNORE_PATTERN.search(l):
|
||||
continue
|
||||
|
||||
print(l, file=sys.stderr)
|
||||
had_error = True
|
||||
|
||||
if had_error:
|
||||
print('Exiting with error')
|
||||
sys.exit(1)
|
||||
|
||||
comment_decl = re.compile(r'\s*///\s*ts: `(.+)`')
|
||||
# Note that the doc attribute is sometimes wrapped onto two lines.
|
||||
attr_decl = re.compile(r'\s*(?:#\[doc\s*=\s*)?"ts: `(.+)`"\]')
|
||||
|
||||
# Make sure /not/ to match arguments with nested parentheses,
|
||||
# which won't survive textual splitting below.
|
||||
function_sig = re.compile(r'(.+)\(([^()]*)\): (.+)')
|
||||
|
||||
for line in stdout.split('\n'):
|
||||
match = comment_decl.match(line) or attr_decl.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
|
||||
(decl,) = match.groups()
|
||||
|
||||
if decl.startswith('export /*trait*/ type '):
|
||||
yield '\n'.join(rewrite_trait(decl, function_sig))
|
||||
continue
|
||||
|
||||
if function_match := function_sig.match(decl):
|
||||
yield rewrite_fn(function_match)
|
||||
continue
|
||||
|
||||
# Fix backslash-escaped double-quotes.
|
||||
yield bytes(decl, 'utf-8').decode('unicode_escape')
|
||||
|
||||
|
||||
def expand_template(template_file: str, decls: Iterable[str]) -> str:
|
||||
decls = list(decls)
|
||||
with open(template_file, 'r') as f:
|
||||
contents = f.read()
|
||||
|
||||
# Rewrite from function syntax to property syntax to take advantage of
|
||||
# https://www.typescriptlang.org/tsconfig/#strictFunctionTypes.
|
||||
contents = contents.replace('NATIVE_FNS;', '\n '.join(
|
||||
rewrite_function_as_property(x.removeprefix('export function '))
|
||||
for x in decls if x.startswith('export function ')
|
||||
))
|
||||
contents = contents.replace('NATIVE_FN_NAMES', ''.join(
|
||||
'\n ' + x.removeprefix('export function ').split('(')[0] + ','
|
||||
for x in decls if x.startswith('export function ')
|
||||
) + '\n')
|
||||
contents = contents.replace('NATIVE_TYPES;', '\n'.join(
|
||||
'export ' + x.removeprefix('export ') for x in decls if not x.startswith('export function ')
|
||||
))
|
||||
|
||||
return contents
|
||||
|
||||
|
||||
def verify_contents(expected_output_file: str, expected_contents: str) -> None:
|
||||
with open(expected_output_file) as fh:
|
||||
current_contents = fh.readlines()
|
||||
diff = difflib.unified_diff(current_contents, expected_contents.splitlines(keepends=True))
|
||||
first_line = next(diff, None)
|
||||
if first_line:
|
||||
sys.stdout.write(first_line)
|
||||
sys.stdout.writelines(diff)
|
||||
sys.exit(f'error: {expected_output_file} not up to date; re-run {sys.argv[0]}!')
|
||||
|
||||
|
||||
Crate = collections.namedtuple('Crate', ['path', 'features'], defaults=[()])
|
||||
|
||||
|
||||
def convert_to_typescript(rust_crates: Iterable[Crate], ts_in_path: str, ts_out_path: str, verify: bool) -> None:
|
||||
decls = itertools.chain.from_iterable(collect_decls(crate.path, crate.features) for crate in rust_crates)
|
||||
contents = expand_template(ts_in_path, decls)
|
||||
|
||||
if not os.access(ts_out_path, os.F_OK):
|
||||
raise Exception(f"Didn't find {ts_out_path} where it was expected")
|
||||
|
||||
if not verify:
|
||||
with open(ts_out_path, 'w') as fh:
|
||||
fh.write(contents)
|
||||
else:
|
||||
verify_contents(ts_out_path, contents)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
our_abs_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
output_file_name = 'Native.ts'
|
||||
|
||||
convert_to_typescript(
|
||||
rust_crates=[
|
||||
Crate(path=os.path.join(our_abs_dir, '..')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared'), features=('node', 'signal-media')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'types'), features=('node', 'signal-media')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'testing'), features=('node', 'signal-media')),
|
||||
],
|
||||
ts_in_path=os.path.join(our_abs_dir, output_file_name + '.in'),
|
||||
ts_out_path=os.path.join(our_abs_dir, '..', '..', '..', '..', 'node', 'ts', output_file_name),
|
||||
verify=args.verify,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
19
rust/bridge/node/native_ts/Cargo.toml
Normal file
19
rust/bridge/node/native_ts/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "libsignal-node-native_ts"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libsignal-bridge = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-bridge-testing = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-bridge-types = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-node = { workspace = true, features = ["node", "metadata"] }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
minijinja = { workspace = true, features = ["preserve_order"] }
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright 2020 Signal Messenger, LLC.
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
@ -134,18 +134,60 @@ export type Serialized<T> = Uint8Array<ArrayBuffer>;
|
||||
type ConnectChatBridge = Wrapper<ConnectionManager>;
|
||||
type TestingFutureCancellationGuard = Wrapper<TestingFutureCancellationCounter>;
|
||||
|
||||
// Keep in sync with rust/bridge/node/src/logging.rs
|
||||
export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
export const NetRemoteConfigKeys = [
|
||||
{%- for key in remote_config_keys -%}
|
||||
'{{ key }}',
|
||||
{%- endfor -%}
|
||||
] as const;
|
||||
|
||||
import load from 'node-gyp-build';
|
||||
|
||||
type NativeFunctions = {
|
||||
registerErrors: (errorsModule: Record<string, unknown>) => void;
|
||||
NATIVE_FNS;
|
||||
initLogger: (maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void) => void;
|
||||
{%- for (name, f) in ctx.native_functions|items %}
|
||||
{{ name }}: (
|
||||
{%- for (name, ty) in f.arguments -%}
|
||||
{{ name }}: {{ ty }},
|
||||
{%- endfor -%}
|
||||
) => {{ f.return_type }};
|
||||
{%- endfor %}
|
||||
};
|
||||
|
||||
const { registerErrors, NATIVE_FN_NAMES } = load(
|
||||
{% macro native_fn_names(ctx) %}
|
||||
{%- for (name, f) in ctx.native_functions|items %}
|
||||
{{ name }},
|
||||
{%- endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
const { registerErrors,
|
||||
initLogger,
|
||||
{{ native_fn_names(ctx) }}
|
||||
} = load(
|
||||
`${import.meta.dirname}/../`
|
||||
) as NativeFunctions;
|
||||
|
||||
export { registerErrors, NATIVE_FN_NAMES };
|
||||
export { registerErrors,
|
||||
initLogger,
|
||||
{{ native_fn_names(ctx)
|
||||
}} };
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
NATIVE_TYPES;
|
||||
{% for (name, fns) in ctx.bridge_traits|items %}
|
||||
export /*trait*/ type {{ name }} = {
|
||||
{%- for fn in fns %}
|
||||
{{ fn.name }}: (
|
||||
{%- for (arg, ty) in fn.body.arguments -%}
|
||||
{{ arg }}: {{ ty }},
|
||||
{%- endfor -%}
|
||||
) => {{ fn.body.return_type }};
|
||||
{%- endfor %}
|
||||
};
|
||||
{% endfor %}
|
||||
|
||||
{% for ty in ctx.opaque_types -%}
|
||||
export interface {{ ty }} { readonly __type: unique symbol; }
|
||||
{% endfor -%}
|
||||
51
rust/bridge/node/native_ts/src/main.rs
Normal file
51
rust/bridge/node/native_ts/src/main.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// To make sure the linkmes work
|
||||
extern crate libsignal_bridge;
|
||||
extern crate libsignal_bridge_testing;
|
||||
extern crate signal_node;
|
||||
|
||||
use clap::Parser;
|
||||
use libsignal_bridge_types::metadata::node::TsMetadataContext;
|
||||
use libsignal_bridge_types::net::remote_config::RemoteConfigKey;
|
||||
use minijinja::context;
|
||||
|
||||
#[derive(Parser)]
|
||||
/// Regenerate Native.ts
|
||||
///
|
||||
/// This command assumes it's being invoked from the workspace root.
|
||||
struct Cli {
|
||||
/// Don't actually overwrite Native.ts, just make sure it's up-to-date.
|
||||
#[clap(long)]
|
||||
verify: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
|
||||
env.add_template("Native.ts.in", include_str!("Native.ts.in"))?;
|
||||
let tmpl = env.get_template("Native.ts.in")?;
|
||||
let mut ctx = TsMetadataContext::default();
|
||||
for item in libsignal_bridge_types::metadata::node::NODE_ITEMS.iter() {
|
||||
// We don't check item.module_path because, unlike other client languages, we emit both
|
||||
// testing and non-testing native into the same typescript file.
|
||||
(item.apply)(&mut ctx);
|
||||
}
|
||||
let code = tmpl.render(context! {
|
||||
ctx => ctx,
|
||||
remote_config_keys => RemoteConfigKey::KEYS,
|
||||
})?;
|
||||
let dst = "./node/ts/Native.ts";
|
||||
if args.verify {
|
||||
anyhow::ensure!(
|
||||
std::fs::read_to_string(dst)? == code,
|
||||
"Native.ts is not up-to-date"
|
||||
);
|
||||
} else {
|
||||
std::fs::write(dst, code.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use futures::executor;
|
||||
use libsignal_bridge::node::{AssumedImmutableBuffer, ResultTypeInfo, SignalNodeError};
|
||||
use libsignal_bridge::node::ResultTypeInfo;
|
||||
use libsignal_bridge::node_register;
|
||||
use libsignal_bridge::support::*;
|
||||
use libsignal_bridge_macros::bridge_fn;
|
||||
@ -16,7 +16,6 @@ use minidump_processor::ProcessorOptions;
|
||||
use minidump_unwind::Symbolizer;
|
||||
use minidump_unwind::symbols::string_symbol_supplier;
|
||||
use neon::prelude::*;
|
||||
use neon::types::buffer::TypedArray;
|
||||
use rand::TryRngCore;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -31,11 +30,6 @@ use libsignal_bridge_testing::*;
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
libsignal_bridge::node::register(&mut cx)?;
|
||||
cx.export_function("initLogger", logging::init_logger)?;
|
||||
cx.export_function(
|
||||
"SealedSenderMultiRecipientMessage_Parse",
|
||||
sealed_sender_multi_recipient_message_parse,
|
||||
)?;
|
||||
cx.export_function("MinidumpToJSONString", minidump_to_json_string)?;
|
||||
let remote_config_keys = libsignal_bridge::net::RemoteConfigKey::KEYS.convert_into(&mut cx)?;
|
||||
cx.export_value("NetRemoteConfigKeys", remote_config_keys)?;
|
||||
Ok(())
|
||||
@ -67,91 +61,96 @@ impl<'a> From<ArrayBuilder<'a>> for Handle<'a, JsArray> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ts: `export function SealedSenderMultiRecipientMessage_Parse(buffer: Uint8Array<ArrayBuffer>): SealedSenderMultiRecipientMessage`
|
||||
fn sealed_sender_multi_recipient_message_parse(mut cx: FunctionContext) -> JsResult<JsObject> {
|
||||
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
|
||||
let buffer = AssumedImmutableBuffer::new(&cx, buffer_arg);
|
||||
let messages = match SealedSenderV2SentMessage::parse(&buffer) {
|
||||
Ok(messages) => messages,
|
||||
Err(e) => {
|
||||
let throwable =
|
||||
e.into_throwable(&mut cx, "sealed_sender_multi_recipient_parse_sent_message");
|
||||
cx.throw(throwable)?
|
||||
}
|
||||
};
|
||||
struct SealedSenderMultiRecipientMessage<'a>(SealedSenderV2SentMessage<'a>);
|
||||
impl<'a, 'b> ResultTypeInfo<'a> for SealedSenderMultiRecipientMessage<'b> {
|
||||
type ResultType = JsObject;
|
||||
|
||||
let recipient_map = cx.empty_object();
|
||||
let mut excluded_recipients_array = ArrayBuilder::new(&mut cx);
|
||||
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
|
||||
let messages = self.0;
|
||||
let recipient_map = cx.empty_object();
|
||||
let mut excluded_recipients_array = ArrayBuilder::new(cx);
|
||||
|
||||
for (service_id, recipient) in &messages.recipients {
|
||||
let service_id_string = cx.string(service_id.service_id_string());
|
||||
if recipient.devices.is_empty() {
|
||||
excluded_recipients_array
|
||||
.push(service_id_string, &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
continue;
|
||||
for (service_id, recipient) in &messages.recipients {
|
||||
let service_id_string = cx.string(service_id.service_id_string());
|
||||
if recipient.devices.is_empty() {
|
||||
excluded_recipients_array
|
||||
.push(service_id_string, cx)
|
||||
.expect("failed to construct output array");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut device_ids = ArrayBuilder::new(cx);
|
||||
let mut registration_ids = ArrayBuilder::new(cx);
|
||||
|
||||
for &(device_id, registration_id) in &recipient.devices {
|
||||
device_ids
|
||||
.push(cx.number(u32::from(device_id)), cx)
|
||||
.expect("failed to construct output array");
|
||||
registration_ids
|
||||
.push(cx.number(registration_id), cx)
|
||||
.expect("failed to construct output array");
|
||||
}
|
||||
|
||||
let range = messages.range_for_recipient_key_material(recipient);
|
||||
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
|
||||
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
|
||||
|
||||
let recipient_object = cx.empty_object();
|
||||
recipient_object
|
||||
.set(cx, "deviceIds", device_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "registrationIds", registration_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "rangeOffset", range_start)
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "rangeLen", range_len)
|
||||
.expect("failed to construct recipient object");
|
||||
|
||||
recipient_map
|
||||
.set(cx, service_id_string, recipient_object)
|
||||
.expect("failed to record recipient object");
|
||||
}
|
||||
|
||||
let mut device_ids = ArrayBuilder::new(&mut cx);
|
||||
let mut registration_ids = ArrayBuilder::new(&mut cx);
|
||||
let offset_of_shared_bytes =
|
||||
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
|
||||
|
||||
for &(device_id, registration_id) in &recipient.devices {
|
||||
device_ids
|
||||
.push(cx.number(u32::from(device_id)), &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
registration_ids
|
||||
.push(cx.number(registration_id), &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
}
|
||||
let result = cx.empty_object();
|
||||
result
|
||||
.set(cx, "recipientMap", recipient_map)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(cx, "excludedRecipients", excluded_recipients_array.into())
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(cx, "offsetOfSharedData", offset_of_shared_bytes)
|
||||
.expect("failed to construct result object");
|
||||
|
||||
let range = messages.range_for_recipient_key_material(recipient);
|
||||
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
|
||||
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
|
||||
|
||||
let recipient_object = cx.empty_object();
|
||||
recipient_object
|
||||
.set(&mut cx, "deviceIds", device_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "registrationIds", registration_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "rangeOffset", range_start)
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "rangeLen", range_len)
|
||||
.expect("failed to construct recipient object");
|
||||
|
||||
recipient_map
|
||||
.set(&mut cx, service_id_string, recipient_object)
|
||||
.expect("failed to record recipient object");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
let offset_of_shared_bytes =
|
||||
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
|
||||
|
||||
let result = cx.empty_object();
|
||||
result
|
||||
.set(&mut cx, "recipientMap", recipient_map)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(
|
||||
&mut cx,
|
||||
"excludedRecipients",
|
||||
excluded_recipients_array.into(),
|
||||
)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(&mut cx, "offsetOfSharedData", offset_of_shared_bytes)
|
||||
.expect("failed to construct result object");
|
||||
|
||||
Ok(result)
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(
|
||||
_: &mut libsignal_bridge_types::metadata::node::TsMetadataContext,
|
||||
) -> String {
|
||||
"SealedSenderMultiRecipientMessage".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// ts: `export function MinidumpToJSONString(buffer: Uint8Array<ArrayBuffer>): string`
|
||||
fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
|
||||
let dump = Minidump::read(buffer_arg.as_slice(&cx)).expect("Failed to parse minidump");
|
||||
#[bridge_fn(jni = false, ffi = false)]
|
||||
fn SealedSenderMultiRecipientMessage_Parse(
|
||||
buffer: &[u8],
|
||||
) -> libsignal_protocol::error::Result<SealedSenderMultiRecipientMessage<'_>> {
|
||||
Ok(SealedSenderMultiRecipientMessage(
|
||||
SealedSenderV2SentMessage::parse(buffer)?,
|
||||
))
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = false, jni = false)]
|
||||
fn MinidumpToJSONString(buffer: &[u8]) -> String {
|
||||
let dump = Minidump::read(buffer).expect("Failed to parse minidump");
|
||||
let provider = Symbolizer::new(string_symbol_supplier(std::collections::HashMap::new()));
|
||||
let options = ProcessorOptions::default();
|
||||
|
||||
@ -165,7 +164,7 @@ fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
.print_json(&mut json, false)
|
||||
.expect("Failed to print json");
|
||||
|
||||
Ok(cx.string(std::str::from_utf8(&json).expect("Failed to convert JSON to utf8")))
|
||||
String::from_utf8(json).expect("Failed to convert JSON to utf8")
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = false, jni = false)]
|
||||
|
||||
@ -9,7 +9,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use libsignal_bridge::node::SimpleArgTypeInfo;
|
||||
use neon::prelude::*;
|
||||
|
||||
/// ts: `export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }`
|
||||
// Keep in sync with Native.ts.in
|
||||
#[derive(Clone, Copy)]
|
||||
enum LogLevel {
|
||||
Error = 1,
|
||||
@ -194,7 +194,6 @@ fn set_max_level_from_js_level(max_level: u32) {
|
||||
log::set_max_level(log::Level::from(level).to_level_filter());
|
||||
}
|
||||
|
||||
/// ts: `export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void): void`
|
||||
pub(crate) fn init_logger(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
let max_level_arg = cx.argument::<JsNumber>(0)?;
|
||||
let max_level = u32::convert_from(&mut cx, max_level_arg)?;
|
||||
|
||||
@ -71,3 +71,4 @@ ffi = ["libsignal-bridge-types/ffi"]
|
||||
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
||||
node = ["neon", "linkme", "libsignal-bridge-types/node"]
|
||||
signal-media = ["dep:signal-media", "libsignal-bridge-types/signal-media"]
|
||||
metadata = ["libsignal-bridge-types/metadata"]
|
||||
|
||||
@ -209,7 +209,7 @@ pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, name: &str) -> Result<To
|
||||
let callbacks = trait_to_bridge
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| bridge_callback_item(name, item))
|
||||
.map(|item| bridge_callback_item(trait_name, name, item))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let callback_aliases = callbacks.iter().map(|c| &c.alias);
|
||||
let callback_fields = callbacks.iter().map(|c| &c.field);
|
||||
@ -264,7 +264,11 @@ struct Callback {
|
||||
forwarding_impl: TokenStream2,
|
||||
}
|
||||
|
||||
fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback> {
|
||||
fn bridge_callback_item(
|
||||
trait_name: &Ident,
|
||||
bridge_name: &str,
|
||||
item: &TraitItem,
|
||||
) -> Result<Callback> {
|
||||
let TraitItem::Fn(item) = item else {
|
||||
return Err(Error::new(item.span(), "only fns are supported"));
|
||||
};
|
||||
@ -281,6 +285,12 @@ fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback>
|
||||
req_name.to_string().to_upper_camel_case(),
|
||||
span = req_name.span()
|
||||
);
|
||||
|
||||
let mut_keyword = item.sig.inputs.first().and_then(|input| match input {
|
||||
FnArg::Receiver(receiver) => receiver.mutability.as_ref(),
|
||||
FnArg::Typed(_) => None,
|
||||
});
|
||||
|
||||
let callback_args = item.sig.inputs.iter().filter_map(|arg| match arg {
|
||||
FnArg::Receiver(_) => match result_info.kind {
|
||||
ResultKind::Regular => Some(quote!(out: *mut ffi_arg_type!(#result_ty))),
|
||||
@ -384,7 +394,7 @@ fn bridge_callback_item(bridge_name: &str, item: &TraitItem) -> Result<Callback>
|
||||
let forwarding_impl = quote! {
|
||||
#[inline]
|
||||
#sig {
|
||||
self.0.#req_name(#(#arg_names),*) #await_if_needed
|
||||
#trait_name::#req_name(& #mut_keyword self.0, #(#arg_names),*) #await_if_needed
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
//! export function SenderKeyMessage_New(
|
||||
//! keyId: number,
|
||||
//! iteration: number,
|
||||
//! ciphertext: Buffer,
|
||||
//! ciphertext: Uint8Array<ArrayBuffer>,
|
||||
//! pk: Wrapper<PrivateKey>
|
||||
//! ): SenderKeyMessage;
|
||||
//! ```
|
||||
@ -132,8 +132,9 @@
|
||||
//!
|
||||
//! 1. Argument and result types for FFI and JNI are determined by macros `ffi_arg_type`,
|
||||
//! `ffi_result_type`, `jni_arg_type`, and `jni_result_type`. You may need to add your new type
|
||||
//! there. JNI and Node types also undergo some additional transformation in the scripts
|
||||
//! `gen_java_decl.py` and `gen_ts_decl.py`, which you may need to tweak as well.
|
||||
//! there. JNI types also undergo some additional transformation in the scripts
|
||||
//! `gen_java_decl.py`, which you may need to tweak as well. Node types are generated as Strings
|
||||
//! via the `gen_ts_ffi()` methods on `node::{AsyncArg, Arg, Result}TypeInfo`.
|
||||
//!
|
||||
//! 2. Argument types conform to one or more of the following bridge-specific traits:
|
||||
//!
|
||||
|
||||
@ -12,7 +12,7 @@ use syn::*;
|
||||
use syn_mid::Signature;
|
||||
|
||||
use crate::BridgingKind;
|
||||
use crate::util::{extract_arg_names_and_types, result_type};
|
||||
use crate::util::{crates, extract_arg_names_and_types, result_type};
|
||||
|
||||
fn bridge_fn_body(orig_name: &Ident, input_args: &[(&Ident, &Type)]) -> TokenStream2 {
|
||||
// Scroll down to the end of the function to see the quote template.
|
||||
@ -182,9 +182,14 @@ pub(crate) fn bridge_fn(
|
||||
let name_with_prefix = format_ident!("node_{}", name);
|
||||
let name_without_prefix = Ident::new(name, Span::call_site());
|
||||
|
||||
let ts_signature_comment = generate_ts_signature_comment(name, sig, bridging_kind);
|
||||
|
||||
let input_args = extract_arg_names_and_types(sig)?;
|
||||
let ts_metadata = generate_ts_metadata(
|
||||
name,
|
||||
sig.asyncness.is_some(),
|
||||
&input_args,
|
||||
result_type(&sig.output),
|
||||
bridging_kind,
|
||||
);
|
||||
|
||||
let body = match (sig.asyncness, bridging_kind) {
|
||||
(Some(_), _) => bridge_fn_async_body(&sig.ident, name, bridging_kind, &input_args),
|
||||
@ -200,51 +205,83 @@ pub(crate) fn bridge_fn(
|
||||
Ok(quote! {
|
||||
#[cfg(feature = "node")]
|
||||
#[allow(non_snake_case)]
|
||||
#[doc = #ts_signature_comment]
|
||||
pub fn #name_with_prefix(
|
||||
mut cx: node::FunctionContext,
|
||||
) -> node::JsResult<node::JsValue> {
|
||||
#body
|
||||
}
|
||||
#[cfg(all(feature = "metadata", feature = "node"))]
|
||||
#ts_metadata
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
node_register!(#name_without_prefix);
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a string, containing the *Rust* signature of a bridged function, that gen_ts_decl.py
|
||||
/// can use to generate Native.d.ts.
|
||||
fn generate_ts_signature_comment(
|
||||
/// Generates the code to embed `libsignal_bridge_types::metadata` metadata
|
||||
fn generate_ts_metadata(
|
||||
name_without_prefix: &str,
|
||||
sig: &Signature,
|
||||
asyncness: bool,
|
||||
input_args: &[(&Ident, &Type)],
|
||||
result_type: TokenStream2,
|
||||
bridging_kind: &BridgingKind,
|
||||
) -> String {
|
||||
let mut ts_args = vec![];
|
||||
) -> TokenStream2 {
|
||||
let krate = crates::libsignal_bridge_types();
|
||||
let mut input_args: Vec<_> = input_args
|
||||
.iter()
|
||||
.map(|(name, ty)| (name.to_string(), ty.to_token_stream()))
|
||||
.collect();
|
||||
match bridging_kind {
|
||||
BridgingKind::Regular => {}
|
||||
BridgingKind::Io { runtime } => {
|
||||
ts_args.push(format!("async_runtime: &{}", runtime.to_token_stream()))
|
||||
let runtime = runtime.to_token_stream();
|
||||
input_args.insert(0, ("async_runtime".to_string(), quote!(&#runtime)))
|
||||
}
|
||||
}
|
||||
ts_args.extend(
|
||||
sig.inputs
|
||||
.iter()
|
||||
.map(|arg| arg.to_token_stream().to_string().replace('\n', " ")),
|
||||
);
|
||||
|
||||
let result_type_format = match (sig.asyncness, bridging_kind) {
|
||||
(Some(_), BridgingKind::Io { .. }) => |ty| format!("CancellablePromise<{ty}>"),
|
||||
(Some(_), _) => |ty| format!("Promise<{ty}>"),
|
||||
(None, _) => |ty| format!("{ty}"),
|
||||
let argument_names = input_args
|
||||
.iter()
|
||||
.map(|(x, _)| to_lower_camel_case_preserve_underscores(x))
|
||||
.collect_vec();
|
||||
let argument_types = input_args.iter().map(|(_, x)| x).collect_vec();
|
||||
let return_type_format = match (asyncness, bridging_kind) {
|
||||
(true, BridgingKind::Io { .. }) => "CancellablePromise<{return_type}>",
|
||||
(true, _) => "Promise<{return_type}>",
|
||||
(false, _) => "{return_type}",
|
||||
};
|
||||
let result_type_str = result_type_format(result_type(&sig.output));
|
||||
let md = quote!(#krate::metadata);
|
||||
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{name_without_prefix}");
|
||||
let type_info_trait = if asyncness {
|
||||
quote!(AsyncArgTypeInfo)
|
||||
} else {
|
||||
quote!(ArgTypeInfo)
|
||||
};
|
||||
quote! {
|
||||
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
|
||||
#[linkme(crate = #md::linkme)]
|
||||
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
|
||||
module_path: module_path!(),
|
||||
apply: |ctx| {
|
||||
use #md::node::result_type_helper::*;
|
||||
let return_type: ResultMetadataTransformHelper<#result_type> = Default::default();
|
||||
let return_type = return_type.register_ts_ffi_type(ctx);
|
||||
let mut arguments = Vec::new();
|
||||
#(arguments.push((
|
||||
#argument_names.into(),
|
||||
<#argument_types as #krate::node::#type_info_trait>::register_ts_ffi_type(ctx)
|
||||
));)*
|
||||
ctx.native_functions.insert(
|
||||
#name_without_prefix.into(),
|
||||
#md::node::NativeFunction { arguments, return_type: format!(#return_type_format) },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
format!(
|
||||
"ts: `export function {}({}): {}`",
|
||||
name_without_prefix,
|
||||
ts_args.join(", "),
|
||||
result_type_str
|
||||
)
|
||||
fn to_lower_camel_case_preserve_underscores(x: &str) -> String {
|
||||
let x_sans_underscore = x.trim_start_matches('_');
|
||||
let core = x_sans_underscore.to_lower_camel_case();
|
||||
format!("{}{core}", &x[0..(x.len() - x_sans_underscore.len())])
|
||||
}
|
||||
|
||||
pub(crate) fn name_from_ident(ident: &Ident) -> String {
|
||||
@ -259,23 +296,20 @@ pub(crate) fn name_from_ident(ident: &Ident) -> String {
|
||||
pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result<TokenStream2> {
|
||||
let trait_name = &trait_to_bridge.ident;
|
||||
let wrapper_name = format_ident!("Node{}", trait_to_bridge.ident);
|
||||
let krate = crates::libsignal_bridge_types();
|
||||
|
||||
let callbacks = trait_to_bridge
|
||||
.items
|
||||
.iter()
|
||||
.map(bridge_callback_item)
|
||||
.map(|x| bridge_callback_item(x, &krate))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let callback_impls = callbacks.iter().map(|c| &c.implementation);
|
||||
let callback_ts_decls = callbacks.iter().map(|c| &c.ts_decl);
|
||||
|
||||
let ts_declaration_comment = format!(
|
||||
"ts: `export /*trait*/ type {js_name} = {{\n{}\n}};`",
|
||||
callback_ts_decls.format("\n")
|
||||
);
|
||||
let callback_bridge_trait_functions = callbacks.iter().map(|c| &c.bridge_trait_function);
|
||||
let md = quote!(#krate::metadata);
|
||||
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{trait_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[cfg(feature = "node")]
|
||||
#[doc = #ts_declaration_comment]
|
||||
pub struct #wrapper_name(node::RootAndChannel);
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -299,15 +333,29 @@ pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result
|
||||
impl #trait_name for #wrapper_name {
|
||||
#(#callback_impls)*
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "node", feature = "metadata"))]
|
||||
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
|
||||
#[linkme(crate = #md::linkme)]
|
||||
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
|
||||
module_path: module_path!(),
|
||||
apply: |ctx| {
|
||||
let mut functions = Vec::new();
|
||||
#(#callback_bridge_trait_functions)*
|
||||
ctx.bridge_traits.insert(#js_name.to_string(), functions);
|
||||
},
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
struct Callback {
|
||||
implementation: TokenStream2,
|
||||
ts_decl: String,
|
||||
/// Push a `node::BridgeTraitFunction` onto the local `functions` Vec
|
||||
/// `ctx: &mut TsMetadataContext` is in scope
|
||||
bridge_trait_function: TokenStream2,
|
||||
}
|
||||
|
||||
fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
fn bridge_callback_item(item: &TraitItem, krate: &TokenStream2) -> Result<Callback> {
|
||||
let TraitItem::Fn(item) = item else {
|
||||
return Err(Error::new(item.span(), "only fns are supported"));
|
||||
};
|
||||
@ -395,21 +443,35 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
}
|
||||
};
|
||||
|
||||
// operation(foo: number): void;
|
||||
let js_arg_decls = item.sig.inputs.iter().filter_map(|arg| match arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
FnArg::Typed(arg) => {
|
||||
let Pat::Ident(arg_name) = &*arg.pat else {
|
||||
// Diagnosed elsewhere.
|
||||
return None;
|
||||
};
|
||||
Some(format!("{}: {}", arg_name.ident, arg.ty.to_token_stream()))
|
||||
}
|
||||
});
|
||||
let args = item
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
FnArg::Typed(arg) => {
|
||||
let Pat::Ident(arg_name) = &*arg.pat else {
|
||||
// Diagnosed elsewhere.
|
||||
return None;
|
||||
};
|
||||
Some((&arg_name.ident, &arg.ty))
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
let arg_names = args
|
||||
.iter()
|
||||
.map(|(x, _)| to_lower_camel_case_preserve_underscores(&x.to_string()))
|
||||
.collect_vec();
|
||||
let arg_types = args.iter().map(|(_, x)| x).collect_vec();
|
||||
let result_ty = result_type(&sig.output);
|
||||
|
||||
let result_string = if sig.asyncness.is_some() {
|
||||
let result_ty = result_type(&sig.output);
|
||||
format!("Promise<{result_ty}>")
|
||||
let return_type = if sig.asyncness.is_some() {
|
||||
quote! {{
|
||||
use #krate::metadata::node::result_type_helper::*;
|
||||
let return_type: CallbackResultMetadataTransformHelper<#result_ty> = Default::default();
|
||||
let return_type = return_type.register_ts_ffi_type(ctx);
|
||||
format!("Promise<{return_type}>")
|
||||
}}
|
||||
} else {
|
||||
if !matches!(sig.output, ReturnType::Default) {
|
||||
return Err(Error::new(
|
||||
@ -417,17 +479,25 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
"non-async callbacks with results are not supported for Node",
|
||||
));
|
||||
}
|
||||
"void".to_owned()
|
||||
quote!("void".to_string())
|
||||
};
|
||||
let ts_decl = format!(
|
||||
"{}({}): {};",
|
||||
js_operation_name,
|
||||
js_arg_decls.format(", "),
|
||||
result_string
|
||||
);
|
||||
|
||||
Ok(Callback {
|
||||
implementation,
|
||||
ts_decl,
|
||||
bridge_trait_function: quote! {
|
||||
let mut arguments = Vec::new();
|
||||
#(arguments.push((
|
||||
#arg_names.to_string(),
|
||||
<#arg_types as #krate::node::ResultTypeInfo>::register_ts_ffi_type(ctx),
|
||||
));)*
|
||||
let return_type = #return_type;
|
||||
functions.push(#krate::metadata::node::BridgeTraitFunction {
|
||||
name: #js_operation_name.to_string(),
|
||||
body: #krate::metadata::node::NativeFunction {
|
||||
arguments,
|
||||
return_type,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -9,6 +9,20 @@ use syn::spanned::Spanned;
|
||||
use syn::*;
|
||||
use syn_mid::{FnArg, Pat, PatType, Signature};
|
||||
|
||||
pub(crate) mod crates {
|
||||
use super::*;
|
||||
fn pkg_name() -> String {
|
||||
std::env::var("CARGO_PKG_NAME").expect("Missing CARGO_PKG_NAME")
|
||||
}
|
||||
pub(crate) fn libsignal_bridge_types() -> TokenStream2 {
|
||||
if pkg_name() == "libsignal-bridge-types" {
|
||||
quote!(crate)
|
||||
} else {
|
||||
quote!(::libsignal_bridge_types)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tokens of the type in `output_as_written`, or `()` if no return type was written.
|
||||
pub(crate) fn result_type(output_as_written: &ReturnType) -> TokenStream2 {
|
||||
match output_as_written {
|
||||
|
||||
@ -125,6 +125,27 @@ async fn UnauthenticatedChatConnection_send(
|
||||
.await
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn UnauthenticatedChatConnection_send_raw_grpc(
|
||||
chat: &UnauthenticatedChatConnection,
|
||||
service: String,
|
||||
method: String,
|
||||
payload: Box<[u8]>,
|
||||
) -> Result<Vec<u8>, RequestError<Infallible>> {
|
||||
chat.as_typed(|chat| {
|
||||
Box::pin(libsignal_net_chat::grpc::raw_grpc(
|
||||
"unauth",
|
||||
chat.0
|
||||
.shared_h2_connection()
|
||||
.expect("requires an H2 connection"),
|
||||
&service,
|
||||
&method,
|
||||
payload.into_vec(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn UnauthenticatedChatConnection_disconnect(chat: &UnauthenticatedChatConnection) {
|
||||
chat.disconnect().await
|
||||
@ -306,6 +327,27 @@ async fn AuthenticatedChatConnection_send(
|
||||
.await
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn AuthenticatedChatConnection_send_raw_grpc(
|
||||
chat: &AuthenticatedChatConnection,
|
||||
service: String,
|
||||
method: String,
|
||||
payload: Box<[u8]>,
|
||||
) -> Result<Vec<u8>, RequestError<Infallible>> {
|
||||
chat.as_typed(|chat| {
|
||||
Box::pin(libsignal_net_chat::grpc::raw_grpc(
|
||||
"auth",
|
||||
chat.0
|
||||
.shared_h2_connection()
|
||||
.expect("requires an H2 connection"),
|
||||
&service,
|
||||
&method,
|
||||
payload.into_vec(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn AuthenticatedChatConnection_disconnect(chat: &AuthenticatedChatConnection) {
|
||||
chat.disconnect().await
|
||||
|
||||
@ -14,8 +14,8 @@ use libsignal_core::{Aci, E164};
|
||||
use libsignal_keytrans::{AccountData, StoredAccountData};
|
||||
use libsignal_net_chat::api::RequestError;
|
||||
use libsignal_net_chat::api::keytrans::{
|
||||
CheckMode, Error, KeyTransparencyClient, MaybePartial, SearchKey, TreeHeadWithTimestamp,
|
||||
UsernameHash, check,
|
||||
AccountDataField, AccountDataFieldReset as _, CheckMode, Error, KeyTransparencyClient,
|
||||
MaybePartial, SearchKey, TreeHeadWithTimestamp, UsernameHash, check,
|
||||
};
|
||||
use libsignal_protocol::PublicKey;
|
||||
use prost::{DecodeError, Message};
|
||||
@ -38,6 +38,20 @@ fn KeyTransparency_UsernameHashSearchKey(hash: &[u8]) -> Vec<u8> {
|
||||
UsernameHash::from_slice(hash).as_search_key()
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn KeyTransparency_ResetDataField(
|
||||
account_data: Box<[u8]>,
|
||||
field: AsType<AccountDataField, u8>,
|
||||
) -> Vec<u8> {
|
||||
// The only failure is decoding error, we'll use empty vec for that.
|
||||
let decoded: Result<StoredAccountData, _> = try_decode(account_data);
|
||||
let Ok(account_data) = decoded else {
|
||||
log::warn!("Failed to decode stored account data");
|
||||
return vec![];
|
||||
};
|
||||
account_data.reset(field.into_inner()).encode_to_vec()
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn KeyTransparency_Check(
|
||||
|
||||
@ -372,20 +372,6 @@ fn SignalMessage_New(
|
||||
)
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = "message_verify_mac")]
|
||||
fn SignalMessage_VerifyMac(
|
||||
msg: &SignalMessage,
|
||||
sender_identity_key: &PublicKey,
|
||||
receiver_identity_key: &PublicKey,
|
||||
mac_key: &[u8],
|
||||
) -> Result<bool> {
|
||||
msg.verify_mac(
|
||||
&IdentityKey::new(*sender_identity_key),
|
||||
&IdentityKey::new(*receiver_identity_key),
|
||||
mac_key,
|
||||
)
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = "message_get_sender_ratchet_key", node = false)]
|
||||
fn SignalMessage_GetSenderRatchetKey(m: &SignalMessage) -> PublicKey {
|
||||
*m.sender_ratchet_key()
|
||||
@ -974,8 +960,35 @@ fn SessionRecord_ArchiveCurrentState(session_record: &mut SessionRecord) -> Resu
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn SessionRecord_HasUsableSenderChain(s: &SessionRecord, now: Timestamp) -> Result<bool> {
|
||||
s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)
|
||||
fn SessionRecord_HasUsableSenderChain(
|
||||
s: &SessionRecord,
|
||||
require_pq_ratio: f64,
|
||||
now: Timestamp,
|
||||
) -> Result<bool> {
|
||||
let has_chain =
|
||||
s.has_usable_sender_chain(now.into(), SessionUsabilityRequirements::NotStale)?;
|
||||
if !has_chain {
|
||||
return Ok(false);
|
||||
}
|
||||
let has_pq_chain = s.has_usable_sender_chain(
|
||||
now.into(),
|
||||
SessionUsabilityRequirements::NotStale
|
||||
| SessionUsabilityRequirements::EstablishedWithPqxdh
|
||||
| SessionUsabilityRequirements::Spqr,
|
||||
)?;
|
||||
if has_pq_chain || require_pq_ratio == 0.0 {
|
||||
return Ok(true);
|
||||
}
|
||||
let require_pq_ratio = if require_pq_ratio > 1.0 {
|
||||
log::warn!("pinning overly high PQ ratio {require_pq_ratio} to 1.0");
|
||||
1.0
|
||||
} else if require_pq_ratio < 0.0 {
|
||||
log::warn!("pinning overly low PQ ratio {require_pq_ratio} to 0.0");
|
||||
0.0
|
||||
} else {
|
||||
require_pq_ratio
|
||||
};
|
||||
Ok(should_use_nonpq_session(require_pq_ratio, s.alice_base_key().expect("we should have a current session, since has_usable_sender_chain returned a non-error value")))
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
|
||||
@ -24,6 +24,7 @@ libsignal-keytrans = { workspace = true }
|
||||
libsignal-message-backup = { workspace = true, features = ["json"] }
|
||||
libsignal-net = { workspace = true }
|
||||
libsignal-net-chat = { workspace = true }
|
||||
libsignal-net-grpc = { workspace = true, features = ["json"] }
|
||||
libsignal-protocol = { workspace = true }
|
||||
zkgroup = { workspace = true }
|
||||
|
||||
@ -54,3 +55,4 @@ ffi = ["libsignal-bridge-types/ffi"]
|
||||
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
||||
node = ["dep:linkme", "dep:neon", "libsignal-bridge-types/node"]
|
||||
signal-media = ["libsignal-bridge-types/signal-media"]
|
||||
metadata = ["libsignal-bridge-types/metadata"]
|
||||
|
||||
@ -73,13 +73,18 @@ async fn TESTING_FakeChatServer_GetNextRemote(server: &FakeChatServer) -> FakeCh
|
||||
fn TESTING_FakeChatConnection_Create(
|
||||
tokio: &TokioAsyncContext,
|
||||
listener: Box<dyn ChatListener>,
|
||||
grpc_overrides_joined_by_newlines: String,
|
||||
alerts_joined_by_newlines: String,
|
||||
) -> FakeChatConnection {
|
||||
// "".split_terminator(...) produces [], while normal split() produces [""].
|
||||
// Leaking is unfortunate, but more expedient than mapping to remote config keys or similar.
|
||||
let grpc_overrides = String::leak(grpc_overrides_joined_by_newlines).split_terminator('\n');
|
||||
let alerts = alerts_joined_by_newlines.split_terminator('\n');
|
||||
|
||||
let (chat, remote) = libsignal_bridge_types::net::chat::FakeChatConnection::new(
|
||||
tokio.handle(),
|
||||
listener.into_event_listener(),
|
||||
grpc_overrides,
|
||||
alerts,
|
||||
);
|
||||
FakeChatConnection {
|
||||
@ -96,7 +101,8 @@ fn TESTING_FakeChatConnection_CreateProvisioning(
|
||||
let (chat, remote) = libsignal_bridge_types::net::chat::FakeChatConnection::new(
|
||||
tokio.handle(),
|
||||
listener.into_event_listener(),
|
||||
vec![],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
FakeChatConnection {
|
||||
chat: Some(chat).into(),
|
||||
@ -159,6 +165,37 @@ fn TESTING_FakeChatRemoteEnd_SendServerResponse(
|
||||
.expect("chat task finished")
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn TESTING_FakeChatRemoteEnd_SendServerGrpcResponse(
|
||||
chat: &FakeChatRemoteEnd,
|
||||
response: &FakeChatResponse,
|
||||
) {
|
||||
let FakeChatResponse(ResponseProto {
|
||||
id,
|
||||
status,
|
||||
message,
|
||||
headers,
|
||||
body,
|
||||
}) = response;
|
||||
|
||||
assert!(
|
||||
message.as_deref().unwrap_or_default().is_empty(),
|
||||
"messages not supported for gRPC"
|
||||
);
|
||||
assert!(headers.is_empty(), "headers not yet implemented for gRPC");
|
||||
|
||||
let http_response = http::Response::builder()
|
||||
.status(u16::try_from(status.unwrap_or_default()).unwrap_or(u16::MAX))
|
||||
.body(body.as_ref().cloned().unwrap_or_default())
|
||||
.expect("valid");
|
||||
|
||||
chat.0
|
||||
.grpc()
|
||||
.await
|
||||
.send_response(id.unwrap_or_default(), http_response)
|
||||
.expect("chat task finished");
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(chat: &FakeChatRemoteEnd) {
|
||||
chat.0
|
||||
@ -203,6 +240,40 @@ async fn TESTING_FakeChatRemoteEnd_ReceiveIncomingRequest(
|
||||
Some((http_request, id.unwrap()))
|
||||
}
|
||||
|
||||
#[bridge_io(TokioAsyncContext)]
|
||||
async fn TESTING_FakeChatRemoteEnd_ReceiveIncomingGrpcRequest(
|
||||
chat: &FakeChatRemoteEnd,
|
||||
) -> Option<(HttpRequest, u64)> {
|
||||
let (id, request) = chat
|
||||
.0
|
||||
.grpc()
|
||||
.await
|
||||
.receive_request()
|
||||
.await
|
||||
.expect("message was invalid")?;
|
||||
let (
|
||||
http::request::Parts {
|
||||
method,
|
||||
uri,
|
||||
headers,
|
||||
..
|
||||
},
|
||||
body,
|
||||
) = request.into_parts();
|
||||
|
||||
let http_request = HttpRequest {
|
||||
method,
|
||||
path: uri
|
||||
.into_parts()
|
||||
.path_and_query
|
||||
.unwrap_or(http::uri::PathAndQuery::from_static("")),
|
||||
body: Some(body),
|
||||
headers: headers.into(),
|
||||
};
|
||||
|
||||
Some((http_request, id))
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_ChatResponseConvert(body_present: bool) -> ChatResponse {
|
||||
let body = match body_present {
|
||||
@ -279,6 +350,52 @@ fn TESTING_FakeChatResponse_Create(
|
||||
})
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_FakeChatRemoteEnd_NextGrpcMessage(input: &[u8], offset: u32) -> (u32, u32) {
|
||||
// Taking an offset avoids extra copies in the streaming input case.
|
||||
let input = &input[offset.try_into().expect("valid offset for buffer")..];
|
||||
let message_slice = libsignal_net_grpc::expect_next_grpc_message_for_testing(input);
|
||||
// We return a (start, end) pair for the app language to slice.
|
||||
// Unfortunately, getting that back out takes a bit of work.
|
||||
let message_offset = if let Some(first_elem) = message_slice.first() {
|
||||
// TODO: replace with slice::element_offset at MSRV 1.94.
|
||||
let first_elem = std::ptr::from_ref(first_elem);
|
||||
let slice_range = input.as_ptr_range();
|
||||
assert!(
|
||||
slice_range.contains(&first_elem),
|
||||
"result should be a subslice"
|
||||
);
|
||||
// Note: subtracting raw addresses only works because the elements are bytes.
|
||||
first_elem.addr() - slice_range.start.addr()
|
||||
} else {
|
||||
// If the message is empty, the header must have been the entire rest of the input.
|
||||
input.len()
|
||||
};
|
||||
let full_offset = offset + u32::try_from(message_offset).expect("input will never be >1GB");
|
||||
(
|
||||
full_offset,
|
||||
full_offset + u32::try_from(message_slice.len()).expect("input will never be >1GB"),
|
||||
)
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_FakeChatRemoteEnd_GrpcFrameForMessageLength(len: u32) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(5);
|
||||
result.push(0);
|
||||
result.extend_from_slice(&len.to_be_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_FakeChatRemoteEnd_BinprotoToJson(name: String, input: &[u8]) -> String {
|
||||
libsignal_net_grpc::json::expect_binproto_to_json_by_name(&name, input)
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_FakeChatRemoteEnd_JsonToBinproto(name: String, input: String) -> Vec<u8> {
|
||||
libsignal_net_grpc::json::expect_json_to_binproto_by_name(&name, &input)
|
||||
}
|
||||
|
||||
make_error_testing_enum! {
|
||||
enum TestingChatConnectError for ConnectError {
|
||||
WebSocket => WebSocketConnectionFailed,
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use libsignal_keytrans::{StoredAccountData, StoredMonitoringData};
|
||||
use libsignal_net_chat::api::RequestError;
|
||||
use libsignal_net_chat::api::keytrans::Error as KeyTransError;
|
||||
use prost::Message;
|
||||
|
||||
use crate::*;
|
||||
|
||||
@ -30,3 +32,23 @@ fn TESTING_KeyTransNonFatalVerificationFailure() -> Result<(), RequestError<KeyT
|
||||
fn TESTING_KeyTransChatSendError() -> Result<(), RequestError<KeyTransError>> {
|
||||
Err(RequestError::Timeout)
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
fn TESTING_KeyTransStoredAccountData() -> Vec<u8> {
|
||||
StoredAccountData {
|
||||
aci: Some(StoredMonitoringData {
|
||||
pos: 1,
|
||||
..Default::default()
|
||||
}),
|
||||
e164: Some(StoredMonitoringData {
|
||||
pos: 2,
|
||||
..Default::default()
|
||||
}),
|
||||
username_hash: Some(StoredMonitoringData {
|
||||
pos: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
last_tree_head: None,
|
||||
}
|
||||
.encode_to_vec()
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ impl ConnectUnauthChat for ConnectFakeChat {
|
||||
| libsignal_net::chat::ws::ListenerEvent::ReceivedMessage(_, _) => (),
|
||||
};
|
||||
|
||||
let (chat, remote) = ChatConnection::new_fake(self.0.clone(), Box::new(listener), []);
|
||||
let (chat, remote) = ChatConnection::new_fake(self.0.clone(), Box::new(listener), [], []);
|
||||
|
||||
std::future::ready(
|
||||
self.1
|
||||
|
||||
@ -105,6 +105,10 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Nee
|
||||
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
Self::None
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -123,6 +127,10 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for NeedsCleanup {
|
||||
// We only want to test that the storage is cleaned up, not the value passed into the wrapped function.
|
||||
Self::None
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but always produces an error when "borrowed" from the
|
||||
@ -162,6 +170,10 @@ impl node::SimpleArgTypeInfo for ErrorOnBorrow {
|
||||
) -> node::NeonResult<Self> {
|
||||
node::Context::throw_type_error(cx, "deliberate error")
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but panics as it is "borrowed" from the app-provided
|
||||
@ -199,6 +211,11 @@ impl node::SimpleArgTypeInfo for PanicOnBorrow {
|
||||
) -> node::NeonResult<Self> {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but panics on the secondary "load" step after the "borrow"
|
||||
@ -258,6 +275,11 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Pan
|
||||
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -276,6 +298,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for PanicOnLoad {
|
||||
fn load_async_arg(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ResultTypeInfo but always fails to produce a result.
|
||||
@ -311,6 +338,11 @@ impl<'a> node::ResultTypeInfo<'a> for ErrorOnReturn {
|
||||
fn convert_into(self, cx: &mut impl node::Context<'a>) -> node::JsResult<'a, Self::ResultType> {
|
||||
cx.throw_type_error("deliberate error")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ResultTypeInfo but always panics when producing a result.
|
||||
@ -347,6 +379,11 @@ impl<'a> node::ResultTypeInfo<'a> for PanicOnReturn {
|
||||
) -> node::JsResult<'a, Self::ResultType> {
|
||||
panic!("deliberate panic");
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(derive_more::Deref)]
|
||||
@ -430,6 +467,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for TestingFutureCancellationGua
|
||||
fn load_async_arg(stored: &'storage mut Self::StoredType) -> Self {
|
||||
stored.take().unwrap().0
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"TestingFutureCancellationGuard".into()
|
||||
}
|
||||
}
|
||||
|
||||
bridge_as_handle!(TestingFutureCancellationCounter);
|
||||
|
||||
@ -84,6 +84,7 @@ jni-type-tagging = []
|
||||
jni-invoke-annotated = []
|
||||
extra-jni-checks = ["jni-type-tagging", "jni-invoke-annotated"]
|
||||
node = ["neon", "linkme", "signal-neon-futures"]
|
||||
metadata = ["linkme", "serde/derive"]
|
||||
|
||||
[target.'cfg(not(any(windows, target_arch = "x86")))'.dependencies]
|
||||
# sha2's asm implementation uses standalone .S files that aren't compiled correctly on Windows,
|
||||
|
||||
@ -563,7 +563,7 @@ impl SimpleArgTypeInfo for &libsignal_account_keys::BackupKey {
|
||||
}
|
||||
|
||||
macro_rules! bridge_trait {
|
||||
($name:ident) => {
|
||||
($name:ident, $load:expr) => {
|
||||
paste! {
|
||||
impl<'a> ArgTypeInfo<'a> for &'a mut dyn $name {
|
||||
type ArgType = crate::ffi::ConstPointer< [<Ffi $name Struct >] >;
|
||||
@ -576,11 +576,14 @@ macro_rules! bridge_trait {
|
||||
}
|
||||
}
|
||||
fn load_from(stored: &'a mut Self::StoredType) -> Self {
|
||||
stored
|
||||
($load)(stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($name:ident) => {
|
||||
bridge_trait!($name, std::convert::identity);
|
||||
};
|
||||
}
|
||||
|
||||
bridge_trait!(IdentityKeyStore);
|
||||
@ -589,8 +592,8 @@ bridge_trait!(SenderKeyStore);
|
||||
bridge_trait!(SessionStore);
|
||||
bridge_trait!(SignedPreKeyStore);
|
||||
bridge_trait!(KyberPreKeyStore);
|
||||
bridge_trait!(InputStream);
|
||||
bridge_trait!(SyncInputStream);
|
||||
bridge_trait!(InputStream, |x: &'a mut Self::StoredType| &mut x.0);
|
||||
bridge_trait!(SyncInputStream, |x: &'a mut Self::StoredType| &mut x.0);
|
||||
|
||||
impl<'a> ArgTypeInfo<'a> for Box<dyn ChatListener> {
|
||||
type ArgType = crate::ffi::ConstPointer<FfiChatListenerStruct>;
|
||||
@ -1426,6 +1429,7 @@ trivial!(u64);
|
||||
trivial!(i64);
|
||||
trivial!(usize);
|
||||
trivial!(bool);
|
||||
trivial!(f64);
|
||||
|
||||
/// Syntactically translates `bridge_fn` argument types (and callback result types) to FFI types for
|
||||
/// `cbindgen`.
|
||||
@ -1441,6 +1445,7 @@ macro_rules! ffi_arg_type {
|
||||
(i32) => (i32);
|
||||
(u32) => (u32);
|
||||
(u64) => (u64);
|
||||
(f64) => (f64);
|
||||
(Option<u32>) => (u32);
|
||||
(usize) => (usize);
|
||||
(bool) => (bool);
|
||||
@ -1546,6 +1551,7 @@ macro_rules! ffi_result_type {
|
||||
(Option<u32>) => (u32);
|
||||
(u64) => (u64);
|
||||
(i64) => (i64);
|
||||
(f64) => (f64);
|
||||
(Option<u64>) => (u64);
|
||||
(bool) => (bool);
|
||||
(&str) => (*const std::ffi::c_char);
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
//
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use std::io;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use libsignal_bridge_macros::bridge_callbacks;
|
||||
|
||||
use crate::ffi;
|
||||
use crate::io::{InputStream, InputStreamRead, SyncInputStream};
|
||||
use crate::support::{BridgedCallbacks, ResultLike, WithContext};
|
||||
|
||||
#[bridge_callbacks(ffi = "FfiInputStream", jni = false, node = false)]
|
||||
trait BridgeInputStream {
|
||||
fn read(&self, buf: &mut [u8]) -> Result<usize, io::Error>;
|
||||
fn skip(&self, amount: u64) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
pub type FfiSyncInputStreamStruct = FfiInputStreamStruct;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<T: BridgeInputStream> InputStream for BridgedCallbacks<T> {
|
||||
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
|
||||
let amount_read = self.0.read(buf)?;
|
||||
Ok(InputStreamRead::Ready { amount_read })
|
||||
}
|
||||
|
||||
async fn skip(&self, amount: u64) -> io::Result<()> {
|
||||
self.0.skip(amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BridgeInputStream> SyncInputStream for BridgedCallbacks<T> {
|
||||
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.0.read(buf)
|
||||
}
|
||||
|
||||
fn skip(&self, amount: u64) -> io::Result<()> {
|
||||
self.0.skip(amount)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@ use std::ffi::CString;
|
||||
use derive_where::derive_where;
|
||||
use libsignal_protocol::*;
|
||||
|
||||
use crate::support::describe_panic;
|
||||
|
||||
#[macro_use]
|
||||
mod convert;
|
||||
pub use convert::*;
|
||||
@ -21,16 +23,15 @@ pub use error::*;
|
||||
mod futures;
|
||||
pub use futures::*;
|
||||
|
||||
mod io;
|
||||
pub use io::*;
|
||||
|
||||
// TODO: This re-export is because of the ffi_arg_type macro expecting all bridging structs to be
|
||||
// TODO: These re-exports are because of the ffi_arg_type macro expecting all bridging structs to be
|
||||
// under the ffi module; eventually we should be able to remove it.
|
||||
pub use crate::io::FfiSyncInputStreamStruct;
|
||||
pub use crate::protocol::storage::{
|
||||
FfiIdentityKeyStoreStruct, FfiKyberPreKeyStoreStruct, FfiPreKeyStoreStruct,
|
||||
FfiSenderKeyStoreStruct, FfiSessionStoreStruct, FfiSignedPreKeyStoreStruct,
|
||||
};
|
||||
use crate::support::describe_panic;
|
||||
|
||||
pub type FfiInputStreamStruct = FfiSyncInputStreamStruct;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NullPointerError;
|
||||
|
||||
@ -10,8 +10,12 @@ use std::task::{Context, Poll};
|
||||
use async_trait::async_trait;
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use futures_util::{AsyncRead, FutureExt as _};
|
||||
use libsignal_bridge_macros::bridge_callbacks;
|
||||
use mediasan_common::{AsyncSkip, Skip};
|
||||
|
||||
use crate::support::{ResultLike, WithContext};
|
||||
use crate::*;
|
||||
|
||||
/// The result of a [`InputStream::read`].
|
||||
///
|
||||
/// This `enum` exists so that [`InputStream::read`] may be implemented as either a sync or async operation. In the
|
||||
@ -61,6 +65,7 @@ pub trait InputStream {
|
||||
}
|
||||
|
||||
/// An input stream of bytes.
|
||||
#[bridge_callbacks(jni = false, node = false)]
|
||||
pub trait SyncInputStream {
|
||||
/// Read an amount of bytes from the input stream.
|
||||
///
|
||||
@ -74,7 +79,7 @@ pub trait SyncInputStream {
|
||||
/// # Errors
|
||||
///
|
||||
/// If an I/O error occurred while reading from the input, an [`io::Error`] is returned.
|
||||
fn read(&self, buf: &mut [u8]) -> io::Result<usize>;
|
||||
fn read(&self, buf: &mut [u8]) -> Result<usize, io::Error>;
|
||||
|
||||
/// Skip an amount of bytes in the input stream.
|
||||
///
|
||||
@ -82,7 +87,20 @@ pub trait SyncInputStream {
|
||||
///
|
||||
/// If the requested number of bytes could not be skipped for any reason, including if the end of stream was
|
||||
/// reached, an error must be returned.
|
||||
fn skip(&self, amount: u64) -> io::Result<()>;
|
||||
fn skip(&self, amount: u64) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
/// Any SyncInputStream is a valid "async" InputStream.
|
||||
#[async_trait(?Send)]
|
||||
impl<T: SyncInputStream> InputStream for T {
|
||||
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
|
||||
let amount_read = self.read(buf)?;
|
||||
Ok(InputStreamRead::Ready { amount_read })
|
||||
}
|
||||
|
||||
async fn skip(&self, amount: u64) -> io::Result<()> {
|
||||
self.skip(amount)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SyncInput<'a> {
|
||||
|
||||
@ -239,6 +239,13 @@ impl SimpleArgTypeInfo<'_> for u64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleArgTypeInfo<'_> for f64 {
|
||||
type ArgType = jdouble;
|
||||
fn convert_from(_env: &mut JNIEnv, foreign: &jdouble) -> Result<Self, BridgeLayerError> {
|
||||
Ok(*foreign)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports values `0..=Long.MAX_VALUE`.
|
||||
///
|
||||
/// Negative `long` values are *not* reinterpreted as large `u64` values.
|
||||
@ -2677,6 +2684,9 @@ macro_rules! jni_arg_type {
|
||||
(u64) => {
|
||||
::jni::sys::jlong
|
||||
};
|
||||
(f64) => {
|
||||
::jni::sys::jdouble
|
||||
};
|
||||
(bool) => {
|
||||
::jni::sys::jboolean
|
||||
};
|
||||
|
||||
@ -6,15 +6,13 @@
|
||||
use std::cell::RefCell;
|
||||
use std::io;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::*;
|
||||
use crate::io::{InputStream, InputStreamRead, SyncInputStream};
|
||||
use crate::io::SyncInputStream;
|
||||
|
||||
pub type JavaInputStream<'a> = JObject<'a>;
|
||||
pub type JavaSyncInputStream<'a> = JObject<'a>;
|
||||
|
||||
/// Implementation of [`InputStream`] for an argument to a bridge function.
|
||||
/// Implementation of [`InputStream`](crate::io::InputStream) for an argument to a bridge function.
|
||||
pub struct JniBridgeInputStream<'a> {
|
||||
env: RefCell<EnvHandle<'a>>,
|
||||
stream: &'a JObject<'a>,
|
||||
@ -111,19 +109,6 @@ impl From<BridgeOrIoError> for IoError {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl InputStream for JniBridgeInputStream<'_> {
|
||||
fn read<'out, 'a: 'out>(&'a self, buf: &mut [u8]) -> io::Result<InputStreamRead<'out>> {
|
||||
let amount_read = self.do_read(buf)?;
|
||||
Ok(InputStreamRead::Ready { amount_read })
|
||||
}
|
||||
|
||||
async fn skip(&self, amount: u64) -> io::Result<()> {
|
||||
self.do_skip(amount)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncInputStream for JniBridgeInputStream<'_> {
|
||||
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
Ok(self.do_read(buf)?)
|
||||
|
||||
@ -20,7 +20,7 @@ pub use jni::objects::{
|
||||
JValueOwned, ReleaseMode,
|
||||
};
|
||||
use jni::objects::{GlobalRef, JThrowable};
|
||||
pub use jni::sys::{jboolean, jint, jlong};
|
||||
pub use jni::sys::{jboolean, jdouble, jint, jlong};
|
||||
use libsignal_account_keys::Error as PinError;
|
||||
use libsignal_core::try_scoped;
|
||||
use libsignal_net::chat::{ConnectError as ChatConnectError, SendError as ChatSendError};
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
pub mod metadata;
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
#[macro_use]
|
||||
pub mod ffi;
|
||||
|
||||
108
rust/bridge/shared/types/src/metadata.rs
Normal file
108
rust/bridge/shared/types/src/metadata.rs
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
//! This module provides metadata about the bridge layer which will be consumed downstream for
|
||||
//! various purposes:
|
||||
//!
|
||||
//! - To emit `Native.ts`, see `libsignal-node-native_ts`
|
||||
//!
|
||||
//! While some metadata facilities are shared, they're specialized to each client language.
|
||||
|
||||
// This is pub so that it can be used in bridge macros.
|
||||
pub use linkme;
|
||||
use linkme::distributed_slice;
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
pub mod node {
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct TsMetadataContext {
|
||||
pub opaque_types: BTreeSet<String>,
|
||||
pub native_functions: BTreeMap<String, NativeFunction>,
|
||||
pub bridge_traits: BTreeMap<String, Vec<BridgeTraitFunction>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NativeFunction {
|
||||
/// (name, type)
|
||||
pub arguments: Vec<(String, String)>,
|
||||
pub return_type: String,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BridgeTraitFunction {
|
||||
pub name: String,
|
||||
pub body: NativeFunction,
|
||||
}
|
||||
|
||||
/// These functions should mutate the attached [TsMetadataContext] to register their item.
|
||||
#[distributed_slice]
|
||||
pub static NODE_ITEMS: [FnWithModule<TsMetadataContext>];
|
||||
|
||||
/// See [crate::support]'s `transform_helper` for how this works, and the rationale.
|
||||
///
|
||||
/// These functions provide the metadata-side (`register_ts_ffi_type()`) of `.ok_if_needed()`
|
||||
///
|
||||
/// ```
|
||||
/// # use libsignal_bridge_types::metadata::node::result_type_helper::*;
|
||||
/// let x: ResultMetadataTransformHelper<i32> = Default::default();
|
||||
/// assert_eq!(x.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
|
||||
/// let y: ResultMetadataTransformHelper<Result<i32, String>> = Default::default();
|
||||
/// assert_eq!(y.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
|
||||
/// ```
|
||||
pub mod result_type_helper {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use derive_where::derive_where;
|
||||
|
||||
use crate::metadata::node::TsMetadataContext;
|
||||
use crate::node::{CallbackResultTypeInfo, ResultTypeInfo};
|
||||
|
||||
#[derive_where(Default)]
|
||||
pub struct ResultMetadataTransformHelper<T>(PhantomData<T>);
|
||||
impl<'a, T: ResultTypeInfo<'a>> ResultMetadataTransformHelper<T> {
|
||||
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
pub trait ResultMetadataTransformHelperTrait {
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
|
||||
}
|
||||
impl<'a, T: ResultTypeInfo<'a>, E> ResultMetadataTransformHelperTrait
|
||||
for ResultMetadataTransformHelper<Result<T, E>>
|
||||
{
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
#[derive_where(Default)]
|
||||
pub struct CallbackResultMetadataTransformHelper<T>(PhantomData<T>);
|
||||
|
||||
impl<T: CallbackResultTypeInfo> CallbackResultMetadataTransformHelper<T> {
|
||||
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
pub trait CallbackResultMetadataTransformHelperTrait {
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
|
||||
}
|
||||
impl<T: CallbackResultTypeInfo, E> CallbackResultMetadataTransformHelperTrait
|
||||
for CallbackResultMetadataTransformHelper<Result<T, E>>
|
||||
{
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FnWithModule<Ctx> {
|
||||
/// The module the function is defined in
|
||||
pub module_path: &'static str,
|
||||
pub apply: fn(&mut Ctx),
|
||||
}
|
||||
@ -423,9 +423,11 @@ impl FakeChatConnection {
|
||||
pub fn new<'a>(
|
||||
tokio_runtime: tokio::runtime::Handle,
|
||||
listener: chat::ws::EventListener,
|
||||
grpc_overrides: impl IntoIterator<Item = &'static str>,
|
||||
alerts: impl IntoIterator<Item = &'a str>,
|
||||
) -> (Self, FakeChatRemote) {
|
||||
let (inner, remote) = ChatConnection::new_fake(tokio_runtime, listener, alerts);
|
||||
let (inner, remote) =
|
||||
ChatConnection::new_fake(tokio_runtime, listener, grpc_overrides, alerts);
|
||||
(Self(inner), remote)
|
||||
}
|
||||
|
||||
@ -533,8 +535,10 @@ async fn establish_chat_connection(
|
||||
proxy_mode,
|
||||
) {
|
||||
(None, DirectOrProxyModeDiscriminants::DirectOnly)
|
||||
| (None, DirectOrProxyModeDiscriminants::DirectThenProxy)
|
||||
| (Some(_), DirectOrProxyModeDiscriminants::ProxyOnly)
|
||||
| (Some(_), DirectOrProxyModeDiscriminants::ProxyThenDirect) => {
|
||||
| (Some(_), DirectOrProxyModeDiscriminants::ProxyThenDirect)
|
||||
| (Some(_), DirectOrProxyModeDiscriminants::DirectThenProxy) => {
|
||||
log::info!("successfully connected {kind} chat")
|
||||
}
|
||||
(None, DirectOrProxyModeDiscriminants::ProxyThenDirect) => log::warn!(
|
||||
|
||||
@ -81,7 +81,6 @@ macro_rules! define_keys {
|
||||
}
|
||||
|
||||
impl RemoteConfigKey {
|
||||
#[doc = concat!("ts: `export const NetRemoteConfigKeys = [", $("'", $key, "', "),* ,"] as const;`")]
|
||||
pub const KEYS: &[&str] = &[$($key),*];
|
||||
#[cfg(test)]
|
||||
const IDENTITIER_KEY_PAIRS: &[(&str, &str)] = &[
|
||||
@ -119,6 +118,7 @@ pub enum RemoteConfigKey {
|
||||
MessagesAnonymousSendSingleRecipientMessage => "grpc.MessagesAnonymousSendSingleRecipientMessage",
|
||||
AttachmentsGetUploadForm => "grpc.AttachmentsGetUploadForm",
|
||||
MessagesSendMessage => "grpc.MessagesSendMessage",
|
||||
BackupsAnonymousGetUploadForm => "grpc.BackupsAnonymousGetUploadForm",
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,6 +299,7 @@ mod tests {
|
||||
let all_known_grpc_keys: HashSet<&str> = std::iter::empty()
|
||||
.chain(services::AccountsAnonymous::iter().map(|x| x.into()))
|
||||
.chain(services::Attachments::iter().map(|x| x.into()))
|
||||
.chain(services::BackupsAnonymous::iter().map(|x| x.into()))
|
||||
.chain(services::KeysAnonymous::iter().map(|x| x.into()))
|
||||
.chain(services::MessagesAnonymous::iter().map(|x| x.into()))
|
||||
.chain(services::Messages::iter().map(|x| x.into()))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,9 @@ impl<T, const LEN: usize> Array<T> for [T; LEN] {
|
||||
pub trait FixedLengthBincodeSerializable: 'static {
|
||||
/// Should be an actual byte array type, like `[u8; 7]`.
|
||||
type Array: Array<u8> + for<'a> TryFrom<&'a [u8], Error = std::array::TryFromSliceError>;
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn name() -> String;
|
||||
}
|
||||
|
||||
/// A wrapper type that indicates that `T` should be serialized across the bridges.
|
||||
|
||||
@ -32,11 +32,13 @@ pub fn validate_serialization<'a, T: Deserialize<'a> + PartialDefault>(
|
||||
macro_rules! bridge_as_fixed_length_serializable {
|
||||
($typ:ident) => {
|
||||
::paste::paste! {
|
||||
// Declare a marker type for TypeScript, the same as bridge_as_handle.
|
||||
// (This is harmless for the other bridges.)
|
||||
#[doc = "ts: `interface " $typ " { readonly __type: unique symbol; }`"]
|
||||
impl FixedLengthBincodeSerializable for $typ {
|
||||
type Array = [u8; [<$typ:snake:upper _LEN>]];
|
||||
#[cfg(feature = "metadata")]
|
||||
fn name() -> String {
|
||||
let name = stringify!($typ);
|
||||
name.rsplit_once("::").map(|(_, x)| x).unwrap_or(name).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -5,4 +5,4 @@
|
||||
|
||||
// The value of this constant is updated by the script
|
||||
// and should not be manually modified
|
||||
pub const VERSION: &str = "0.93.2";
|
||||
pub const VERSION: &str = "0.94.1";
|
||||
|
||||
@ -312,6 +312,20 @@ pub struct MonitoringData {
|
||||
pub owned: bool,
|
||||
/// Search key
|
||||
pub search_key: Vec<u8>,
|
||||
/// Greatest counter observed in any proof step received for this search
|
||||
/// key.
|
||||
///
|
||||
/// It includes counter values only present in frontier nodes that
|
||||
/// are _not_ used in the monitoring path (only covers the ancestor nodes)
|
||||
/// Tracked separately from `ptrs` to keep the monitoring algorithm
|
||||
/// unmodified and following the spec precisely.
|
||||
///
|
||||
/// Max observed version is only used for the early version change
|
||||
/// detection in `libsignal_net_chat::api::keytrans::monitor_and_search`.
|
||||
/// Without it the version change will only be detected by monitor after
|
||||
/// the tree root has moved to a node that includes the update, which
|
||||
/// requires the log to grow by a lot.
|
||||
pub max_observed_version: u32,
|
||||
}
|
||||
|
||||
impl Debug for MonitoringData {
|
||||
@ -322,6 +336,7 @@ impl Debug for MonitoringData {
|
||||
ptrs,
|
||||
owned,
|
||||
search_key,
|
||||
max_observed_version,
|
||||
} = self;
|
||||
|
||||
let redact_bytes = |bytes: &[u8]| {
|
||||
@ -339,6 +354,7 @@ impl Debug for MonitoringData {
|
||||
.field("ptrs", &ptrs)
|
||||
.field("owned", &owned)
|
||||
.field("search_key", &redact_bytes(search_key))
|
||||
.field("max_observed_version", &max_observed_version)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@ -367,22 +383,33 @@ impl MonitoringData {
|
||||
|
||||
/// The greatest known version of the search key.
|
||||
pub fn greatest_version(&self) -> u32 {
|
||||
self.ptrs
|
||||
*self
|
||||
.ptrs
|
||||
.values()
|
||||
.chain([&self.max_observed_version])
|
||||
.max()
|
||||
.copied()
|
||||
.expect("at least one version must be present")
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitoringData {
|
||||
fn into_stored(self, search_key: Vec<u8>) -> StoredMonitoringData {
|
||||
let Self {
|
||||
index,
|
||||
pos,
|
||||
ptrs,
|
||||
owned,
|
||||
// Prefer the search key provided as argument.
|
||||
search_key: _,
|
||||
max_observed_version,
|
||||
} = self;
|
||||
StoredMonitoringData {
|
||||
index: self.index.into(),
|
||||
pos: self.pos,
|
||||
ptrs: self.ptrs,
|
||||
owned: self.owned,
|
||||
index: index.into(),
|
||||
pos,
|
||||
ptrs,
|
||||
owned,
|
||||
search_key,
|
||||
max_observed_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -395,6 +422,7 @@ impl From<StoredMonitoringData> for MonitoringData {
|
||||
ptrs,
|
||||
owned,
|
||||
search_key,
|
||||
max_observed_version,
|
||||
} = value;
|
||||
Self {
|
||||
index: index.try_into().expect("must be the right size"),
|
||||
@ -402,6 +430,7 @@ impl From<StoredMonitoringData> for MonitoringData {
|
||||
ptrs,
|
||||
owned,
|
||||
search_key,
|
||||
max_observed_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ message StoredMonitoringData {
|
||||
map<uint64, uint32> ptrs = 3;
|
||||
bool owned = 4;
|
||||
bytes search_key = 5;
|
||||
uint32 max_observed_version = 6;
|
||||
}
|
||||
|
||||
message StoredAccountData {
|
||||
|
||||
@ -807,6 +807,7 @@ impl MonitoringDataWrapper {
|
||||
ptrs: HashMap::from([(ver_pos, version)]),
|
||||
owned,
|
||||
search_key: vec![],
|
||||
max_observed_version: version,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -860,6 +861,10 @@ impl MonitoringDataWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
if version > data.max_observed_version {
|
||||
data.max_observed_version = version;
|
||||
}
|
||||
|
||||
if !data.owned && owned {
|
||||
data.owned = true;
|
||||
}
|
||||
@ -898,6 +903,15 @@ impl MonitoringDataWrapper {
|
||||
|
||||
data.ptrs = ptrs;
|
||||
|
||||
// Keep the max_observed_version up to date and tracking the maximum
|
||||
// version across _all_ nodes, not just the ancestor ones used for
|
||||
// `ptrs`.
|
||||
if let Some(max_version) = tree_mapping.versions().flatten().max()
|
||||
&& max_version > data.max_observed_version
|
||||
{
|
||||
data.max_observed_version = max_version;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -964,6 +978,14 @@ impl VersionExtractor<'_> {
|
||||
.map(|step| Ok(get_proto_field(&step.prefix, "prefix")?.counter))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn versions(&self) -> impl Iterator<Item = Option<u32>> {
|
||||
self.0.values().map(|step| {
|
||||
get_proto_field(&step.prefix, "prefix")
|
||||
.ok()
|
||||
.map(|x| x.counter)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn into_sorted_pairs<K: Ord + Copy, V>(map: HashMap<K, V>) -> (Vec<K>, Vec<V>) {
|
||||
@ -1125,6 +1147,7 @@ mod test {
|
||||
ptrs: HashMap::from_iter([(16777215, 2)]),
|
||||
owned: true,
|
||||
search_key: vec![],
|
||||
max_observed_version: 2,
|
||||
}));
|
||||
// These values were obtained by running the integration test in
|
||||
// rust/net/chat/src/api/keytrans.rs and extracting positions and versions
|
||||
@ -1205,6 +1228,7 @@ mod test {
|
||||
ptrs: HashMap::from([(10, 1)]),
|
||||
owned: true,
|
||||
search_key: vec![],
|
||||
max_observed_version: 1,
|
||||
}));
|
||||
|
||||
let steps = proof_steps([(11, 1), (15, 2)]);
|
||||
@ -1223,6 +1247,7 @@ mod test {
|
||||
ptrs: HashMap::from([(10, 1)]),
|
||||
owned: true,
|
||||
search_key: vec![],
|
||||
max_observed_version: 1,
|
||||
}));
|
||||
// later position contains a smaller version
|
||||
let steps = proof_steps([(11, 0)]);
|
||||
@ -1239,6 +1264,7 @@ mod test {
|
||||
ptrs: HashMap::from([(10, 1)]),
|
||||
owned: true,
|
||||
search_key: vec![],
|
||||
max_observed_version: 1,
|
||||
}));
|
||||
|
||||
let steps = HashMap::from_iter([
|
||||
@ -1259,6 +1285,7 @@ mod test {
|
||||
ptrs: HashMap::from([(10, 1), (11, 2)]),
|
||||
owned: true,
|
||||
search_key: vec![],
|
||||
max_observed_version: 2,
|
||||
}));
|
||||
let steps = proof_steps([(11, 3)]);
|
||||
let result = wrapper.update(16, &steps);
|
||||
@ -1330,4 +1357,48 @@ mod test {
|
||||
let result = extractor.get(1);
|
||||
assert_matches!(result, Err(Error::RequiredFieldMissing(s)) => assert!(s.contains("prefix")));
|
||||
}
|
||||
|
||||
// A counter increment that's only visible at a frontier proof (e.g. a
|
||||
// recent tombstone for a contact's old E.164) must still be reflected in
|
||||
// `MonitoringData::greatest_version`, so that the version-change detector
|
||||
// in `monitor_then_search` triggers a follow-up search.
|
||||
//
|
||||
// For a tree of size 20 with `first_pos = 0`:
|
||||
// ```text
|
||||
// root(0, 20) = 15
|
||||
// monitoring_path(10, 0, 20) = [11, 15] (ancestors > 10)
|
||||
// full_monitoring_path = [11, 15, 19] (adds frontier node 19)
|
||||
// ```
|
||||
// A tombstone at log position 17 is not yet in the prefix tree at sizes
|
||||
// 12 or 16, so the ancestor proofs at positions 11 and 15 report the
|
||||
// stored counter (0). It _is_ in the prefix tree at size 20, so the
|
||||
// frontier proof at position 19 reports the updated counter (1). The
|
||||
// ancestor-only walk in `find_updated_mapping` never visits the frontier
|
||||
// node, so `ptrs` does not change. The greatest known version of
|
||||
// the search key, however, should reflect the higher counter so that
|
||||
// version change detection still works.
|
||||
#[test]
|
||||
fn frontier_only_version_increment_is_detected() {
|
||||
let mut wrapper = MonitoringDataWrapper::new(Some(MonitoringData {
|
||||
index: [0; 32],
|
||||
pos: 0,
|
||||
ptrs: HashMap::from([(10, 0)]),
|
||||
owned: false,
|
||||
search_key: vec![],
|
||||
max_observed_version: 0,
|
||||
}));
|
||||
|
||||
let steps = proof_steps([
|
||||
// Ancestor nodes
|
||||
(11, 0),
|
||||
(15, 0),
|
||||
// Frontier node with updated version counter
|
||||
(19, 1),
|
||||
]);
|
||||
|
||||
wrapper.update(20, &steps).expect("valid test data");
|
||||
let data = wrapper.inner.expect("valid test data");
|
||||
|
||||
assert_eq!(1, data.greatest_version(),);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,11 +35,15 @@ derive-where = { workspace = true }
|
||||
derive_more = { workspace = true, features = ["debug", "from", "try_from"] }
|
||||
displaydoc = { workspace = true }
|
||||
either = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
hkdf = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
http = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hyper = { workspace = true, features = ["server"] } # for FakeChatRemote
|
||||
hyper-util = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nonzero_ext = { workspace = true }
|
||||
@ -77,9 +81,6 @@ libsignal-net = { path = ".", features = ["test-util"] }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
either = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
hyper-util = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
@ -68,6 +68,7 @@ nonzero_ext = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
test-log = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "test-util"] }
|
||||
|
||||
@ -19,7 +19,7 @@ use libsignal_keytrans::{
|
||||
AccountData, ChatDistinguishedResponse, ChatMonitorResponse, ChatSearchResponse,
|
||||
CondensedTreeSearchResponse, FullSearchResponse, FullTreeHead, KeyTransparency, LastTreeHead,
|
||||
LocalStateUpdate, MonitorContext, MonitorKey, MonitorProof, MonitorRequest, MonitorResponse,
|
||||
SearchContext, SearchStateUpdate, SlimSearchRequest,
|
||||
SearchContext, SearchStateUpdate, SlimSearchRequest, StoredAccountData,
|
||||
};
|
||||
use libsignal_net::env::KeyTransConfig;
|
||||
use libsignal_protocol::PublicKey;
|
||||
@ -538,6 +538,20 @@ impl UnauthenticatedChatApi for KeyTransparencyClient<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AccountDataFieldReset {
|
||||
fn reset(self, field: AccountDataField) -> Self;
|
||||
}
|
||||
|
||||
impl AccountDataFieldReset for StoredAccountData {
|
||||
fn reset(mut self, field: AccountDataField) -> Self {
|
||||
match field {
|
||||
AccountDataField::E164 => self.e164 = None,
|
||||
AccountDataField::UsernameHash => self.username_hash = None,
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_support {
|
||||
use std::cell::Cell;
|
||||
@ -758,8 +772,9 @@ pub(crate) mod test_support {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
use libsignal_keytrans::StoredMonitoringData;
|
||||
use prost::Message as _;
|
||||
use test_case::test_case;
|
||||
use test_case::{test_case, test_matrix};
|
||||
|
||||
use super::test_support::{
|
||||
CHAT_SEARCH_RESPONSE, CHAT_SEARCH_RESPONSE_VALID_AT, KEYTRANS_CONFIG_STAGING, test_account,
|
||||
@ -859,4 +874,37 @@ mod test {
|
||||
assert_eq!(skip.to_vec(), missing_fields.into_iter().collect::<Vec<_>>())
|
||||
);
|
||||
}
|
||||
|
||||
#[test_matrix([AccountDataField::E164, AccountDataField::UsernameHash])]
|
||||
fn reset_account_data_field(field: AccountDataField) {
|
||||
let field_data = StoredMonitoringData::default();
|
||||
let data = StoredAccountData {
|
||||
aci: None,
|
||||
e164: Some(StoredMonitoringData {
|
||||
pos: 1,
|
||||
..field_data.clone()
|
||||
}),
|
||||
username_hash: Some(StoredMonitoringData {
|
||||
pos: 2,
|
||||
..field_data
|
||||
}),
|
||||
last_tree_head: None,
|
||||
};
|
||||
|
||||
let updated = data.clone().reset(field);
|
||||
|
||||
match field {
|
||||
AccountDataField::E164 => {
|
||||
assert!(updated.e164.is_none());
|
||||
assert_matches!(
|
||||
updated.username_hash,
|
||||
Some(StoredMonitoringData { pos: 2, .. })
|
||||
);
|
||||
}
|
||||
AccountDataField::UsernameHash => {
|
||||
assert_matches!(updated.e164, Some(StoredMonitoringData { pos: 1, .. }));
|
||||
assert!(updated.username_hash.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,16 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A tag identifying an optional field in [`libsignal_keytrans::AccountData`]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display)]
|
||||
#[repr(u8)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display, derive_more::TryFrom,
|
||||
)]
|
||||
#[try_from(repr)]
|
||||
pub enum AccountDataField {
|
||||
/// E.164
|
||||
E164,
|
||||
E164 = 0,
|
||||
/// Username hash
|
||||
UsernameHash,
|
||||
UsernameHash = 1,
|
||||
}
|
||||
|
||||
/// This struct adds to its type parameter a (potentially empty) list of
|
||||
|
||||
@ -346,6 +346,25 @@ impl SearchVersions {
|
||||
.flatten()
|
||||
.max()
|
||||
}
|
||||
|
||||
fn short_description(&self) -> String {
|
||||
fn opt(x: &Option<impl ToString>) -> String {
|
||||
x.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or("_".to_string())
|
||||
}
|
||||
let Self {
|
||||
aci,
|
||||
e164,
|
||||
username_hash,
|
||||
} = self;
|
||||
format!(
|
||||
"[aci: {}, e164: {}, username_hash: {}]",
|
||||
opt(aci),
|
||||
opt(e164),
|
||||
opt(username_hash)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Action<'a> {
|
||||
@ -693,6 +712,8 @@ async fn monitor_then_search<'a>(
|
||||
distinguished_tree_head: &LastTreeHead,
|
||||
mode: CheckMode,
|
||||
) -> Result<MaybePartial<AccountData>, RequestError<Error>> {
|
||||
let stored_versions = SearchVersions::from_account_data(&stored_account_data);
|
||||
log::info!("Stored versions: {}", stored_versions.short_description());
|
||||
let monitor_account_data = {
|
||||
let Parameters {
|
||||
aci,
|
||||
@ -711,8 +732,8 @@ async fn monitor_then_search<'a>(
|
||||
// Call to `monitor` guarantees that the optionality of E.164 and username hash data
|
||||
// will match between `stored_account_data` and `monitor_account_data`. Meaning, they will
|
||||
// either both be Some() or both None.
|
||||
let stored_versions = SearchVersions::from_account_data(&stored_account_data);
|
||||
let updated_versions = SearchVersions::from_account_data(&monitor_account_data);
|
||||
log::info!("Updated versions: {}", stored_versions.short_description());
|
||||
let version_delta = updated_versions
|
||||
.try_subtract(&stored_versions)
|
||||
.map_err(|_| {
|
||||
@ -765,8 +786,8 @@ mod test {
|
||||
use test_case::{test_case, test_matrix};
|
||||
|
||||
use super::{
|
||||
Action, Parameters, PostMonitorAction, TreeHeadWithTimestamp, VersionChanged, check,
|
||||
is_too_old, merge_account_data, modal_search, monitor_then_search,
|
||||
Action, Parameters, PostMonitorAction, SearchVersions, TreeHeadWithTimestamp,
|
||||
VersionChanged, check, is_too_old, merge_account_data, modal_search, monitor_then_search,
|
||||
select_baseline_tree_head,
|
||||
};
|
||||
use crate::api::RequestError;
|
||||
@ -2036,4 +2057,17 @@ mod test {
|
||||
ControlFlow::Break(expected),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_versions_as_short_string() {
|
||||
assert_eq!(
|
||||
"[aci: 42, e164: _, username_hash: 73]",
|
||||
SearchVersions {
|
||||
aci: Some(42),
|
||||
e164: None,
|
||||
username_hash: Some(73),
|
||||
}
|
||||
.short_description()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,68 @@ impl<T: GrpcService + Clone + Sync> GrpcServiceProvider for T {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tonic encoder and decoder that passes byte buffers through unchanged, letting tonic
|
||||
/// add the gRPC framing and nothing else.
|
||||
struct PassthroughCodec;
|
||||
|
||||
impl tonic::codec::Codec for PassthroughCodec {
|
||||
type Encode = Vec<u8>;
|
||||
type Decode = Vec<u8>;
|
||||
type Encoder = Self;
|
||||
type Decoder = Self;
|
||||
|
||||
fn encoder(&mut self) -> Self::Encoder {
|
||||
PassthroughCodec
|
||||
}
|
||||
fn decoder(&mut self) -> Self::Decoder {
|
||||
PassthroughCodec
|
||||
}
|
||||
}
|
||||
|
||||
impl tonic::codec::Encoder for PassthroughCodec {
|
||||
type Item = Vec<u8>;
|
||||
type Error = tonic::Status;
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: Self::Item,
|
||||
dst: &mut tonic::codec::EncodeBuf<'_>,
|
||||
) -> Result<(), Self::Error> {
|
||||
use bytes::BufMut;
|
||||
dst.put(&item[..]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl tonic::codec::Decoder for PassthroughCodec {
|
||||
type Item = Vec<u8>;
|
||||
type Error = tonic::Status;
|
||||
fn decode(
|
||||
&mut self,
|
||||
src: &mut tonic::codec::DecodeBuf<'_>,
|
||||
) -> Result<Option<Self::Item>, Self::Error> {
|
||||
use bytes::Buf;
|
||||
Ok(Some(src.copy_to_bytes(src.remaining()).into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raw_grpc(
|
||||
log_tag: &'static str,
|
||||
service_provider: impl GrpcServiceProvider,
|
||||
service_name: &str,
|
||||
method: &str,
|
||||
payload: Vec<u8>,
|
||||
) -> impl Future<Output = Result<Vec<u8>, RequestError<Infallible>>> {
|
||||
let mut client = tonic::client::Grpc::new(service_provider.service());
|
||||
let path = http::uri::PathAndQuery::from_maybe_shared(format!("/{service_name}/{method}"))
|
||||
.expect("valid URI path");
|
||||
log_and_send(log_tag, method, || async move {
|
||||
let response = client
|
||||
.unary(tonic::Request::new(payload), path, PassthroughCodec)
|
||||
.await?;
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
}
|
||||
|
||||
async fn log_and_send<F, R, E>(
|
||||
log_tag: &'static str,
|
||||
log_safe_description: &str,
|
||||
@ -314,7 +376,7 @@ fn request_error_from_server_side_error_info<E>(
|
||||
}
|
||||
"RESOURCE_EXHAUSTED" | "UNAVAILABLE" => {
|
||||
// UNAVAILABLE is unlikely to have RetryInfo, but it doesn't really hurt to check.
|
||||
if let Some(mut retry_delay) =
|
||||
if let Some(retry_delay) =
|
||||
matching_details::<google::rpc::RetryInfo>(&grpc_status.details)
|
||||
.at_most_one()
|
||||
.unwrap_or_else(|mut e| {
|
||||
@ -325,10 +387,23 @@ fn request_error_from_server_side_error_info<E>(
|
||||
})
|
||||
.and_then(|info| info.retry_delay)
|
||||
{
|
||||
retry_delay.normalize();
|
||||
// TODO: Use i32::div_ceil when that's stabilized.
|
||||
// https://github.com/rust-lang/rust/issues/88581
|
||||
fn nanos_to_secs_ceil(dividend: i32) -> i32 {
|
||||
const DIVISOR: i32 = 1_000_000_000;
|
||||
// Normal Div rounds towards 0.
|
||||
let result = dividend / DIVISOR;
|
||||
if dividend > 0 && dividend % DIVISOR != 0 {
|
||||
result + 1
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Round up so that we're guaranteed to wait *at least* this long.
|
||||
let retry_after_seconds =
|
||||
retry_delay.seconds + i64::from(retry_delay.nanos.clamp(0, 1));
|
||||
let retry_after_seconds = retry_delay
|
||||
.seconds
|
||||
.saturating_add(nanos_to_secs_ceil(retry_delay.nanos).into());
|
||||
return RequestError::RetryLater(RetryLater {
|
||||
retry_after_seconds: u32::try_from(
|
||||
retry_after_seconds.clamp(0, u32::MAX.into()),
|
||||
@ -424,10 +499,14 @@ pub(crate) mod testutil {
|
||||
use crate::api::testutil::TEST_SELF_ACI;
|
||||
use crate::ws::WsConnection;
|
||||
|
||||
pub(crate) fn req(uri: &str, body: impl prost::Message + 'static) -> http::Request<Vec<u8>> {
|
||||
let body = tonic::codec::EncodeBody::new_client(
|
||||
tonic_prost::ProstEncoder::new(Default::default()),
|
||||
futures_util::stream::iter([Ok(body)]),
|
||||
pub(crate) fn encode_for_grpc<C: tonic::codec::Encoder<Error = Status>>(
|
||||
encoder: C,
|
||||
item: C::Item,
|
||||
) -> Vec<u8> {
|
||||
// The difference between client and server only seems to matter when using compression.
|
||||
tonic::codec::EncodeBody::new_client(
|
||||
encoder,
|
||||
futures_util::stream::iter([Ok(item)]),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@ -436,8 +515,11 @@ pub(crate) mod testutil {
|
||||
.expect("non-blocking encoding")
|
||||
.expect("can read entire message")
|
||||
.to_bytes()
|
||||
.into();
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn req(uri: &str, body: impl prost::Message + 'static) -> http::Request<Vec<u8>> {
|
||||
let body = encode_for_grpc(tonic_prost::ProstEncoder::new(Default::default()), body);
|
||||
req_typed(uri, body)
|
||||
}
|
||||
|
||||
@ -475,6 +557,10 @@ pub(crate) mod testutil {
|
||||
Status::new(code, "").into_http()
|
||||
}
|
||||
|
||||
/// Validates that the [`WsConnection`] implementation of an API defers to the gRPC
|
||||
/// implementation when the `message` override is provided.
|
||||
///
|
||||
/// Then defers to the inner validator for further checking and producing a response.
|
||||
pub(crate) struct GrpcOverrideRequestValidator<V> {
|
||||
pub(crate) validator: V,
|
||||
pub(crate) message: &'static str,
|
||||
@ -506,6 +592,13 @@ pub(crate) mod testutil {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that a gRPC request matches in all parts of the underlying HTTP request, checking
|
||||
/// the body byte-for-byte.
|
||||
///
|
||||
/// Prefer a [`GrpcOverrideRequestValidator`] containing a `RequestValidator` if the request has
|
||||
/// a corresponding config to switch between WS and gRPC implementations. Replace the
|
||||
/// `RequestValidator` with `TypedRequestValidator` if comparing the bodies using protobuf
|
||||
/// semantics (rather than bytewise) is important---it usually isn't.
|
||||
pub(crate) struct RequestValidator {
|
||||
pub expected: http::Request<Vec<u8>>,
|
||||
pub response: http::Response<Vec<u8>>,
|
||||
@ -554,7 +647,10 @@ pub(crate) mod testutil {
|
||||
/// Like `RequestValidator`, but compares the decoded protobuf of the incoming request instead
|
||||
/// of the serialized bytes.
|
||||
///
|
||||
/// Prefer `RequestValidator`
|
||||
/// Prefer `RequestValidator` if the protobuf does not contain any `map` fields, because it also
|
||||
/// checks that there are no extraneous fields in the body. (While protobuf permits fields to
|
||||
/// appear in any order, our prost implementation is consistent within a build, if not
|
||||
/// necessarily across versions. `map` is only a problem because it uses Rust's HashMap.)
|
||||
pub(crate) struct TypedRequestValidator<T> {
|
||||
pub expected: http::Request<T>,
|
||||
pub response: http::Response<Vec<u8>>,
|
||||
@ -598,6 +694,29 @@ pub(crate) mod testutil {
|
||||
}
|
||||
}
|
||||
|
||||
/// Use to check that no gRPC calls happen at all (e.g. for a `should_panic` test, but don't
|
||||
/// forget to check the panic message in that case!).
|
||||
pub(crate) struct UnreachableValidator;
|
||||
|
||||
impl tower_service::Service<http::Request<tonic::body::Body>> for &'_ UnreachableValidator {
|
||||
type Response = http::Response<http_body_util::Full<bytes::Bytes>>;
|
||||
|
||||
type Error = hyper::Error;
|
||||
|
||||
type Future = std::future::Pending<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
unreachable!("should not attempt to send");
|
||||
}
|
||||
|
||||
fn call(&mut self, _req: http::Request<tonic::body::Body>) -> Self::Future {
|
||||
unreachable!("should not attempt to send");
|
||||
}
|
||||
}
|
||||
|
||||
/// A protoscope-like helper type for decoding arbitrary protobuf messages.
|
||||
///
|
||||
/// Always succeeds as long as the input is not malformed. Only intended for debugging.
|
||||
@ -887,13 +1006,13 @@ mod test {
|
||||
fn test_retry_later(reason: &str) {
|
||||
let info = vec![
|
||||
google::rpc::RetryInfo {
|
||||
retry_delay: Some(prost_types::Duration {
|
||||
retry_delay: Some(libsignal_net_grpc::Duration {
|
||||
seconds: 10,
|
||||
nanos: 2,
|
||||
}),
|
||||
},
|
||||
google::rpc::RetryInfo {
|
||||
retry_delay: Some(prost_types::Duration {
|
||||
retry_delay: Some(libsignal_net_grpc::Duration {
|
||||
seconds: 20,
|
||||
nanos: 5,
|
||||
}),
|
||||
|
||||
@ -151,12 +151,13 @@ mod test {
|
||||
use std::fmt::Debug;
|
||||
|
||||
use futures_util::FutureExt as _;
|
||||
use libsignal_net_grpc::proto::chat::services;
|
||||
use test_case::test_case;
|
||||
|
||||
use super::*;
|
||||
use crate::api::backups::UnauthenticatedChatApi;
|
||||
use crate::api::testutil::fixed_seed_test_rng;
|
||||
use crate::grpc::testutil::{RequestValidator, err, ok, req};
|
||||
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
|
||||
|
||||
/// A variation of `==` that ignores header order, since the gRPC encoding of this type uses a
|
||||
/// protobuf map for the headers, which is not guaranteed to preserve order.
|
||||
@ -212,19 +213,22 @@ mod test {
|
||||
fn test_get_upload_form(
|
||||
response: http::Response<Vec<u8>>,
|
||||
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
|
||||
GetUploadFormRequest {
|
||||
signed_presentation: Some(SignedPresentation {
|
||||
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
|
||||
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
|
||||
}),
|
||||
upload_type: Some(UploadType::Messages(MessagesUploadType {})),
|
||||
upload_length: 12345,
|
||||
},
|
||||
),
|
||||
response,
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::BackupsAnonymous::GetUploadForm.into(),
|
||||
validator: RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
|
||||
GetUploadFormRequest {
|
||||
signed_presentation: Some(SignedPresentation {
|
||||
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
|
||||
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
|
||||
}),
|
||||
upload_type: Some(UploadType::Messages(MessagesUploadType {})),
|
||||
upload_length: 12345,
|
||||
},
|
||||
),
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
Unauth(&validator)
|
||||
@ -273,19 +277,22 @@ mod test {
|
||||
fn test_get_media_upload_form(
|
||||
response: http::Response<Vec<u8>>,
|
||||
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
|
||||
GetUploadFormRequest {
|
||||
signed_presentation: Some(SignedPresentation {
|
||||
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
|
||||
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
|
||||
}),
|
||||
upload_length: 12345,
|
||||
upload_type: Some(UploadType::Media(MediaUploadType {})),
|
||||
},
|
||||
),
|
||||
response,
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::BackupsAnonymous::GetUploadForm.into(),
|
||||
validator: RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.backup.BackupsAnonymous/GetUploadForm",
|
||||
GetUploadFormRequest {
|
||||
signed_presentation: Some(SignedPresentation {
|
||||
presentation: BackupAuth::EXPECTED_PRESENTATION.to_vec(),
|
||||
presentation_signature: BackupAuth::EXPECTED_SIGNATURE.to_vec(),
|
||||
}),
|
||||
upload_length: 12345,
|
||||
upload_type: Some(UploadType::Media(MediaUploadType {})),
|
||||
},
|
||||
),
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
Unauth(&validator)
|
||||
|
||||
@ -599,8 +599,8 @@ mod test {
|
||||
use crate::api::testutil::{SERIALIZED_GROUP_SEND_TOKEN, structurally_valid_group_send_token};
|
||||
use crate::api::{ChallengeOption, RateLimitChallenge};
|
||||
use crate::grpc::testutil::{
|
||||
GrpcOverrideRequestValidator, RequestValidator, TypedRequestValidator, err, ok, req,
|
||||
req_typed,
|
||||
GrpcOverrideRequestValidator, RequestValidator, TypedRequestValidator,
|
||||
UnreachableValidator, err, ok, req, req_typed,
|
||||
};
|
||||
|
||||
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
|
||||
@ -778,14 +778,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "online-only")]
|
||||
fn ephemeral_story_is_not_allowed() {
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.messages.MessagesAnonymous/SendMultiRecipientStory",
|
||||
SendMultiRecipientStoryRequest::default(),
|
||||
),
|
||||
response: err(tonic::Code::FailedPrecondition),
|
||||
};
|
||||
|
||||
let validator = UnreachableValidator;
|
||||
_ = Unauth(&validator)
|
||||
.send_multi_recipient_message(
|
||||
vec![1, 2, 3].into(),
|
||||
@ -1149,14 +1142,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "online-only")]
|
||||
fn ephemeral_story_is_not_allowed_single_recipient() {
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.messages.MessagesAnonymous/SendStory",
|
||||
SendMultiRecipientStoryRequest::default(),
|
||||
),
|
||||
response: err(tonic::Code::FailedPrecondition),
|
||||
};
|
||||
|
||||
let validator = UnreachableValidator;
|
||||
_ = Unauth(&validator)
|
||||
.send_message(
|
||||
Pni::from(PNI_UUID).into(),
|
||||
|
||||
@ -49,26 +49,23 @@ impl<T: GrpcServiceProvider> crate::api::profiles::UnauthenticatedAccountExisten
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_account_exists {
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
use futures_util::FutureExt;
|
||||
use libsignal_core::{Aci, Pni};
|
||||
use libsignal_net_grpc::proto::chat::services;
|
||||
use test_case::test_case;
|
||||
use test_case::test_matrix;
|
||||
use uuid::{Uuid, uuid};
|
||||
|
||||
use super::*;
|
||||
use crate::api::profiles::UnauthenticatedAccountExistenceApi;
|
||||
use crate::grpc::testutil::{self, GrpcOverrideRequestValidator, RequestValidator, req};
|
||||
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
|
||||
|
||||
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
|
||||
const PNI_UUID: Uuid = uuid!("796abedb-ca4e-4f18-8803-1fde5b921f9f");
|
||||
|
||||
#[test_case(Aci::from(ACI_UUID).into(), true)]
|
||||
#[test_case(Pni::from(PNI_UUID).into(), true)]
|
||||
#[test_case(Aci::from(ACI_UUID).into(), false)]
|
||||
#[test_case(Pni::from(PNI_UUID).into(), false)]
|
||||
#[tokio::test]
|
||||
async fn test_it(service_id: ServiceId, found: bool) {
|
||||
#[test_matrix([Aci::from(ACI_UUID).into(), Pni::from(PNI_UUID).into()], [false, true])]
|
||||
fn test_account_exists(service_id: ServiceId, found: bool) {
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::AccountsAnonymous::CheckAccountExistence.into(),
|
||||
validator: RequestValidator {
|
||||
@ -78,7 +75,7 @@ mod test_account_exists {
|
||||
service_identifier: Some(service_id.into()),
|
||||
},
|
||||
),
|
||||
response: testutil::ok(CheckAccountExistenceResponse {
|
||||
response: ok(CheckAccountExistenceResponse {
|
||||
account_exists: found,
|
||||
}),
|
||||
},
|
||||
@ -91,8 +88,8 @@ mod test_account_exists {
|
||||
assert_eq!(result, found);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid() {
|
||||
#[test]
|
||||
fn test_account_exists_invalid() {
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::AccountsAnonymous::CheckAccountExistence.into(),
|
||||
validator: RequestValidator {
|
||||
@ -102,7 +99,7 @@ mod test_account_exists {
|
||||
service_identifier: Some(Aci::from(ACI_UUID).into()),
|
||||
},
|
||||
),
|
||||
response: testutil::err(tonic::Code::DeadlineExceeded),
|
||||
response: err(tonic::Code::DeadlineExceeded),
|
||||
},
|
||||
};
|
||||
let result = Unauth(&validator)
|
||||
@ -110,6 +107,6 @@ mod test_account_exists {
|
||||
.now_or_never()
|
||||
.expect("sync")
|
||||
.expect_err("should fail");
|
||||
assert!(matches!(result, RequestError::Timeout));
|
||||
assert_matches!(result, RequestError::Timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,12 +123,13 @@ mod test {
|
||||
use data_encoding_macro::base64url_nopad;
|
||||
use futures_util::FutureExt as _;
|
||||
use libsignal_net_grpc::proto::chat::common::{IdentityType, ServiceIdentifier};
|
||||
use libsignal_net_grpc::proto::chat::services;
|
||||
use test_case::test_case;
|
||||
use uuid::{Uuid, uuid};
|
||||
|
||||
use super::*;
|
||||
use crate::api::usernames::UnauthenticatedChatApi;
|
||||
use crate::grpc::testutil::{RequestValidator, err, ok, req};
|
||||
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
|
||||
|
||||
const ACI_UUID: Uuid = uuid!("9d0652a3-dcc3-4d11-975f-74d61598733f");
|
||||
|
||||
@ -169,14 +170,17 @@ mod test {
|
||||
// Not realistic, but not likely to show up by accident.
|
||||
let hash = &[0x00, 0xff, 0xff, 0xff];
|
||||
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameHash",
|
||||
LookupUsernameHashRequest {
|
||||
username_hash: hash.to_vec(),
|
||||
},
|
||||
),
|
||||
response,
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::AccountsAnonymous::LookupUsernameHash.into(),
|
||||
validator: RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameHash",
|
||||
LookupUsernameHashRequest {
|
||||
username_hash: hash.to_vec(),
|
||||
},
|
||||
),
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
Unauth(&validator)
|
||||
@ -205,14 +209,17 @@ mod test {
|
||||
fn test_link_lookup(
|
||||
response: http::Response<Vec<u8>>,
|
||||
) -> Result<Option<String>, RequestError<usernames::UsernameLinkError>> {
|
||||
let validator = RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameLink",
|
||||
LookupUsernameLinkRequest {
|
||||
username_link_handle: uuid::Uuid::nil().as_bytes().to_vec(),
|
||||
},
|
||||
),
|
||||
response,
|
||||
let validator = GrpcOverrideRequestValidator {
|
||||
message: services::AccountsAnonymous::LookupUsernameLink.into(),
|
||||
validator: RequestValidator {
|
||||
expected: req(
|
||||
"/org.signal.chat.account.AccountsAnonymous/LookupUsernameLink",
|
||||
LookupUsernameLinkRequest {
|
||||
username_link_handle: uuid::Uuid::nil().as_bytes().to_vec(),
|
||||
},
|
||||
),
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
Unauth(&validator)
|
||||
|
||||
@ -356,6 +356,7 @@ mod testutil {
|
||||
tokio::runtime::Handle::current(),
|
||||
DropOnDisconnect::new(on_disconnect).into_listener(),
|
||||
[],
|
||||
[],
|
||||
);
|
||||
async {
|
||||
let _ignore_failure = self.remote.send(fake_remote);
|
||||
|
||||
@ -554,6 +554,7 @@ mod test {
|
||||
tokio::runtime::Handle::current(),
|
||||
DropOnDisconnect::new(on_disconnect).into_listener(),
|
||||
[],
|
||||
[],
|
||||
);
|
||||
fake_chat_tx.send(fake_remote).unwrap();
|
||||
Ok(Unauth(fake_chat))
|
||||
@ -634,7 +635,7 @@ mod test {
|
||||
remote: fake_chat_remote_tx,
|
||||
};
|
||||
|
||||
let (request_sender, _join_handle) = spawn_connected_chat(&fake_connect)
|
||||
let (request_sender, join_handle) = spawn_connected_chat(&fake_connect)
|
||||
.await
|
||||
.expect("can connect");
|
||||
let fake_chat_remote = fake_chat_remote_rx.recv().await.unwrap();
|
||||
@ -686,6 +687,7 @@ mod test {
|
||||
let _response = first_send_fut.await;
|
||||
|
||||
// The task should reach its inactivity timeout and disconnect.
|
||||
join_handle.await.expect("no panic");
|
||||
assert_matches!(fake_chat_remote.receive_request().await, Ok(None));
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,12 @@ impl<T: WsConnection> crate::api::backups::UnauthenticatedChatApi<OverWs> for Un
|
||||
upload_size: u64,
|
||||
rng: &mut (dyn rand::CryptoRng + Send),
|
||||
) -> Result<UploadForm, RequestError<GetUploadFormFailure>> {
|
||||
if let Some(grpc) =
|
||||
self.grpc_service_to_use_instead(services::BackupsAnonymous::GetUploadForm.into())
|
||||
{
|
||||
return Unauth(grpc).get_upload_form(auth, upload_size, rng).await;
|
||||
}
|
||||
|
||||
let auth = auth.present(rng)?;
|
||||
|
||||
let response = self
|
||||
|
||||
@ -5,17 +5,32 @@ edition = "2021"
|
||||
authors = ["Signal Messenger LLC"]
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[features]
|
||||
json = [
|
||||
"dep:pbjson",
|
||||
"dep:pbjson-build",
|
||||
"dep:pbjson-types",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
libsignal-core = { workspace = true }
|
||||
|
||||
const-str = { workspace = true }
|
||||
derive-where = { workspace = true }
|
||||
pbjson = { workspace = true, optional = true }
|
||||
pbjson-types = { workspace = true, optional = true }
|
||||
prost = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
tonic = { workspace = true, default-features = false, features = ["codegen"] }
|
||||
tonic-prost = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
pbjson-build = { workspace = true, optional = true }
|
||||
tonic-prost-build = { workspace = true }
|
||||
|
||||
[lib]
|
||||
|
||||
@ -35,9 +35,32 @@ fn main() {
|
||||
service_method_file.push("service_methods.rs");
|
||||
std::fs::write(service_method_file, service_method_contents).expect("can write to OUT_DIR");
|
||||
|
||||
tonic_prost_build::configure()
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
let mut json_build = pbjson_build::Builder::new();
|
||||
for fd in &fds.file {
|
||||
json_build.register_file_descriptor(fd.clone());
|
||||
}
|
||||
json_build
|
||||
.build(&[".org.signal.chat"])
|
||||
.expect("can compile with pbjson");
|
||||
}
|
||||
|
||||
let mut tonic_build = tonic_prost_build::configure()
|
||||
.build_server(false)
|
||||
.build_transport(false)
|
||||
.build_transport(false);
|
||||
if cfg!(feature = "json") {
|
||||
tonic_build = tonic_build
|
||||
.compile_well_known_types(true)
|
||||
.extern_path(".google.protobuf", "::pbjson_types")
|
||||
// Note that this diverges from proper protobuf JSON in the interest of simplicity and
|
||||
// prost_types compatibility. (Empty would normally be encoded as `{}`, but `()` is
|
||||
// encoded as `null`.)
|
||||
.extern_path(".google.protobuf.Empty", "()")
|
||||
// These are only used for generic errors, not requests and responses.
|
||||
.extern_path(".google.protobuf.Any", "::prost_types::Any");
|
||||
}
|
||||
tonic_build
|
||||
.compile_fds_with_config(fds, config)
|
||||
.expect("can generate code");
|
||||
}
|
||||
|
||||
69
rust/net/grpc/src/json.rs
Normal file
69
rust/net/grpc/src/json.rs
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub fn expect_binproto_to_json<T: prost::Message + Default + serde::Serialize>(
|
||||
input: &[u8],
|
||||
) -> String {
|
||||
serde_json::to_string(&T::decode(input).expect("valid input")).expect("can encode as JSON")
|
||||
}
|
||||
|
||||
pub fn expect_json_to_binproto<T: prost::Message + serde::de::DeserializeOwned>(
|
||||
input: &str,
|
||||
) -> Vec<u8> {
|
||||
serde_json::from_str::<T>(input)
|
||||
.expect("valid JSON")
|
||||
.encode_to_vec()
|
||||
}
|
||||
|
||||
pub fn expect_binproto_to_json_by_name(message_name: &str, input: &[u8]) -> String {
|
||||
type BinprotoToJsonFn = fn(&[u8]) -> String;
|
||||
// TODO: generate this
|
||||
static OPS: LazyLock<HashMap<&'static str, BinprotoToJsonFn>> = LazyLock::new(|| {
|
||||
HashMap::from_iter([
|
||||
(
|
||||
"org.signal.chat.account.LookupUsernameHashRequest",
|
||||
expect_binproto_to_json::<crate::proto::chat::account::LookupUsernameHashRequest>
|
||||
as _,
|
||||
),
|
||||
(
|
||||
"org.signal.chat.account.LookupUsernameLinkRequest",
|
||||
expect_binproto_to_json::<crate::proto::chat::account::LookupUsernameLinkRequest>
|
||||
as _,
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
let op = OPS
|
||||
.get(message_name)
|
||||
.unwrap_or_else(|| unimplemented!("missing binproto_to_json for {message_name}"));
|
||||
op(input)
|
||||
}
|
||||
|
||||
pub fn expect_json_to_binproto_by_name(message_name: &str, input: &str) -> Vec<u8> {
|
||||
type JsonToBinprotoFn = fn(&str) -> Vec<u8>;
|
||||
// TODO: generate this
|
||||
static OPS: LazyLock<HashMap<&'static str, JsonToBinprotoFn>> = LazyLock::new(|| {
|
||||
HashMap::from_iter([
|
||||
(
|
||||
"org.signal.chat.account.LookupUsernameHashResponse",
|
||||
expect_json_to_binproto::<crate::proto::chat::account::LookupUsernameHashResponse>
|
||||
as _,
|
||||
),
|
||||
(
|
||||
"org.signal.chat.account.LookupUsernameLinkResponse",
|
||||
expect_json_to_binproto::<crate::proto::chat::account::LookupUsernameLinkResponse>
|
||||
as _,
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
let op = OPS
|
||||
.get(message_name)
|
||||
.unwrap_or_else(|| unimplemented!("missing json_to_binproto for {message_name}"));
|
||||
op(input)
|
||||
}
|
||||
@ -5,28 +5,45 @@
|
||||
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
pub mod json;
|
||||
|
||||
pub mod proto {
|
||||
pub mod chat {
|
||||
pub mod common {
|
||||
tonic::include_proto!("org.signal.chat.common");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.common.serde");
|
||||
}
|
||||
pub mod errors {
|
||||
tonic::include_proto!("org.signal.chat.errors");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.errors.serde");
|
||||
}
|
||||
pub mod account {
|
||||
tonic::include_proto!("org.signal.chat.account");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.account.serde");
|
||||
}
|
||||
pub mod attachments {
|
||||
tonic::include_proto!("org.signal.chat.attachments");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.attachments.serde");
|
||||
}
|
||||
pub mod backup {
|
||||
tonic::include_proto!("org.signal.chat.backup");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.backup.serde");
|
||||
}
|
||||
pub mod device {
|
||||
tonic::include_proto!("org.signal.chat.device");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.device.serde");
|
||||
}
|
||||
pub mod messages {
|
||||
tonic::include_proto!("org.signal.chat.messages");
|
||||
#[cfg(feature = "json")]
|
||||
tonic::include_proto!("org.signal.chat.messages.serde");
|
||||
}
|
||||
|
||||
// Not actually a proto, we just make sure to generate our helper file in the same place.
|
||||
@ -49,6 +66,11 @@ pub mod proto {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "json"))]
|
||||
pub type Duration = prost_types::Duration;
|
||||
#[cfg(feature = "json")]
|
||||
pub type Duration = pbjson_types::Duration;
|
||||
|
||||
impl From<libsignal_core::ServiceId> for proto::chat::common::ServiceIdentifier {
|
||||
fn from(value: libsignal_core::ServiceId) -> Self {
|
||||
let kind = match value.kind() {
|
||||
@ -139,3 +161,23 @@ impl prost::Name for proto::google::rpc::RetryInfo {
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Manual implementation of the gRPC framing format (Length-Prefixed-Message).
|
||||
///
|
||||
/// tonic normally takes care of this for us on the Rust side, but app-level tests (using e.g.
|
||||
/// `FakeChatRemote`) have to deal with the raw HTTP bodies.
|
||||
///
|
||||
/// See <https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md>.
|
||||
pub fn expect_next_grpc_message_for_testing(input: &[u8]) -> &[u8] {
|
||||
const HEADER_LEN: usize = 5;
|
||||
assert!(input.len() >= HEADER_LEN, "unexpected EOF");
|
||||
assert_eq!(input[0], 0, "compression not supported");
|
||||
let message_length =
|
||||
u32::from_be_bytes(*input[1..].first_chunk().expect("already checked length"));
|
||||
let message_length = usize::try_from(message_length).expect("at least 32-bit usize");
|
||||
assert!(
|
||||
message_length + HEADER_LEN <= input.len(),
|
||||
"message length exceeds remaining input"
|
||||
);
|
||||
&input[HEADER_LEN..][..message_length]
|
||||
}
|
||||
|
||||
@ -209,6 +209,10 @@ impl<Transport: UsesTransport<UnresolvedTransportRoute>> DescribeForLog
|
||||
},
|
||||
inner: _,
|
||||
}) => (target_host.as_informational_host(), *target_port),
|
||||
ConnectionProxyRoute::Reflector(reflector) => (
|
||||
Host::Domain(reflector.target_host.clone()),
|
||||
DEFAULT_HTTPS_PORT,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -14,8 +14,8 @@ use crate::certs::RootCertificates;
|
||||
use crate::errors::LogSafeDisplay;
|
||||
use crate::host::Host;
|
||||
use crate::route::{
|
||||
ReplaceFragment, RouteProvider, RouteProviderContext, SimpleRoute, TcpRoute, TlsRoute,
|
||||
TlsRouteFragment, UnresolvedHost,
|
||||
HttpsTlsRoute, ReplaceFragment, RouteProvider, RouteProviderContext, SimpleRoute, TcpRoute,
|
||||
TlsRoute, TlsRouteFragment, UnresolvedHost, WebSocketRoute,
|
||||
};
|
||||
use crate::tcp_ssl::proxy::socks;
|
||||
use crate::{Alpn, OverrideNagleAlgorithm};
|
||||
@ -53,6 +53,12 @@ pub struct HttpProxyAuth {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ReflectorProxyRoute<Addr> {
|
||||
pub outer: WebSocketRoute<HttpsTlsRoute<TlsRoute<TcpRoute<Addr>>>>,
|
||||
pub target_host: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, strum::EnumDiscriminants)]
|
||||
#[strum_discriminants(name(ConnectionProxyKind))]
|
||||
pub enum ConnectionProxyRoute<Addr> {
|
||||
@ -66,6 +72,8 @@ pub enum ConnectionProxyRoute<Addr> {
|
||||
},
|
||||
Socks(SocksRoute<Addr>),
|
||||
Https(HttpsProxyRoute<Addr>),
|
||||
// Boxed because it's much larger than the other variants.
|
||||
Reflector(Box<ReflectorProxyRoute<Addr>>),
|
||||
}
|
||||
|
||||
/// Target address for proxy protocols that support remote resolution.
|
||||
@ -95,6 +103,7 @@ pub enum DirectOrProxyMode {
|
||||
DirectOnly,
|
||||
ProxyOnly(ConnectionProxyConfig),
|
||||
ProxyThenDirect(ConnectionProxyConfig),
|
||||
DirectThenProxy(ConnectionProxyConfig),
|
||||
}
|
||||
|
||||
/// [`RouteProvider`] implementation that returns [`DirectOrProxyRoute`]s.
|
||||
@ -137,6 +146,9 @@ pub struct HttpProxy {
|
||||
pub resolve_hostname_locally: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReflectorProviderConfig;
|
||||
|
||||
#[derive(Debug, Clone, derive_more::From)]
|
||||
pub enum ConnectionProxyConfig {
|
||||
Tls(TlsProxy),
|
||||
@ -144,6 +156,10 @@ pub enum ConnectionProxyConfig {
|
||||
Tcp(TcpProxy),
|
||||
Socks(SocksProxy),
|
||||
Http(HttpProxy),
|
||||
/// Reflector tunnel providers to try. The caller is expected to take this
|
||||
/// slice from the surrounding environment/domain config so prod and staging
|
||||
/// can't be mispaired.
|
||||
Reflector(&'static [ReflectorProviderConfig]),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, displaydoc::Display)]
|
||||
@ -258,11 +274,14 @@ impl ConnectionProxyConfig {
|
||||
}
|
||||
|
||||
pub fn is_signal_transparent_proxy(&self) -> bool {
|
||||
// Here, a "signal_transparent_proxy" is one we don't want to fall back
|
||||
// from on connect failure. Currently that's just `Self::Tls`.
|
||||
// TODO(reflector): clean up this method naming.
|
||||
match self {
|
||||
Self::Tls(_) => true,
|
||||
#[cfg(feature = "dev-util")]
|
||||
Self::Tcp(_) => true,
|
||||
Self::Socks(_) | Self::Http(_) => false,
|
||||
Self::Socks(_) | Self::Http(_) | Self::Reflector(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,7 +341,9 @@ where
|
||||
let replacer = move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy);
|
||||
Either::Right(Either::Left(original_routes.map(replacer)))
|
||||
}
|
||||
DirectOrProxyMode::ProxyThenDirect(proxy) => {
|
||||
DirectOrProxyMode::ProxyThenDirect(proxy)
|
||||
| DirectOrProxyMode::DirectThenProxy(proxy) => {
|
||||
let direct_first = matches!(mode, DirectOrProxyMode::DirectThenProxy(_));
|
||||
let original_routes = original_routes.collect_vec();
|
||||
let direct_routes = original_routes
|
||||
.iter()
|
||||
@ -330,18 +351,22 @@ where
|
||||
.map(|r| r.replace(DirectOrProxyRoute::Direct))
|
||||
.collect_vec();
|
||||
let replacer = proxy.as_replacer();
|
||||
let replacer = move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy);
|
||||
Either::Right(Either::Right(
|
||||
original_routes
|
||||
.into_iter()
|
||||
.map(replacer)
|
||||
.chain(direct_routes),
|
||||
))
|
||||
let proxied_routes = original_routes
|
||||
.into_iter()
|
||||
.map(move |r: D::Route| replacer(r).replace(DirectOrProxyRoute::Proxy))
|
||||
.collect_vec();
|
||||
let (first, second) = if direct_first {
|
||||
(direct_routes, proxied_routes)
|
||||
} else {
|
||||
(proxied_routes, direct_routes)
|
||||
};
|
||||
Either::Right(Either::Right(first.into_iter().chain(second)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(reflector): deep nesting of `Either` can be optimized away later.
|
||||
trait AsReplacer {
|
||||
fn as_replacer<R: ReplaceFragment<TcpRoute<UnresolvedHost>>>(
|
||||
&self,
|
||||
@ -354,26 +379,32 @@ impl AsReplacer for ConnectionProxyConfig {
|
||||
) -> impl Fn(R) -> R::Replacement<ConnectionProxyRoute<Host<UnresolvedHost>>> {
|
||||
let replacer = match self {
|
||||
ConnectionProxyConfig::Tls(tls_proxy) => {
|
||||
Either::Left(Either::Left(tls_proxy.as_replacer()))
|
||||
Either::Left(Either::Left(Either::Left(tls_proxy.as_replacer())))
|
||||
}
|
||||
#[cfg(feature = "dev-util")]
|
||||
ConnectionProxyConfig::Tcp(tcp_proxy) => {
|
||||
Either::Right(Either::Left(tcp_proxy.as_replacer()))
|
||||
Either::Left(Either::Right(Either::Left(tcp_proxy.as_replacer())))
|
||||
}
|
||||
ConnectionProxyConfig::Socks(socks_proxy) => {
|
||||
let replacer = socks_proxy.as_replacer();
|
||||
#[cfg(feature = "dev-util")]
|
||||
let replacer = Either::Right(replacer);
|
||||
Either::Right(replacer)
|
||||
Either::Left(Either::Right(replacer))
|
||||
}
|
||||
ConnectionProxyConfig::Http(http_proxy) => {
|
||||
Either::Left(Either::Right(http_proxy.as_replacer()))
|
||||
Either::Left(Either::Left(Either::Right(http_proxy.as_replacer())))
|
||||
}
|
||||
// TODO(reflector): reshape replacement API to handle reflectors expansion.
|
||||
ConnectionProxyConfig::Reflector(_providers) => Either::Right(
|
||||
|_route: R| -> R::Replacement<ConnectionProxyRoute<Host<UnresolvedHost>>> {
|
||||
unimplemented!("reflector route expansion not yet implemented")
|
||||
},
|
||||
),
|
||||
};
|
||||
move |route| match &replacer {
|
||||
Either::Left(Either::Left(f)) => f(route),
|
||||
Either::Left(Either::Right(f)) => f(route),
|
||||
Either::Right(f) => match f {
|
||||
Either::Left(Either::Left(Either::Left(f))) => f(route),
|
||||
Either::Left(Either::Left(Either::Right(f))) => f(route),
|
||||
Either::Left(Either::Right(f)) => match f {
|
||||
#[cfg(feature = "dev-util")]
|
||||
Either::Left(f) => f(route),
|
||||
#[cfg(feature = "dev-util")]
|
||||
@ -381,6 +412,7 @@ impl AsReplacer for ConnectionProxyConfig {
|
||||
#[cfg(not(feature = "dev-util"))]
|
||||
f => f(route),
|
||||
},
|
||||
Either::Right(f) => f(route),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user