diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b23ed3c..5a4cf4d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,22 +13,22 @@ jobs: build: strategy: matrix: - # The "reproducible" entry is used to build the project with the LTS Java version used in reproducible builds script. + # java="25" is the LTS Java version used in reproducible builds script (default in Containerfile). # More Java versions can be added to test compatibility, eg. "26". - java: ["reproducible", "26"] + java: ["25", "26"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build run: | - if [ "${{ matrix.java }}" != "reproducible" ]; then + if [ "${{ matrix.java }}" != "25" ]; then export OVERRIDE_JAVA_VERSION="${{ matrix.java }}" fi ./reproducible-builds/build.sh - name: Upload build artifacts uses: actions/upload-artifact@v7 with: - name: signal-cli-${{ matrix.java }}-${{ github.job }} + name: signal-cli-archive-${{ matrix.java }} path: dist/* build-client: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a18b953f..621581f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,11 @@ env: IMAGE_REGISTRY: ghcr.io/asamk REGISTRY_USER: ${{ github.actor }} REGISTRY_PASSWORD: ${{ github.token }} + ARCHIVE_JAVA_VERSION: 25 jobs: build: - uses: AsamK/signal-cli/.github/workflows/build.yml@master + uses: ./.github/workflows/build.yml release: needs: build @@ -31,7 +32,7 @@ jobs: - name: Get signal-cli version id: version run: | - mv ./signal-cli-reproducible-build/* . + mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* . echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - name: Create release @@ -87,7 +88,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz mkdir -p build/install/ mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli @@ -127,7 +128,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz mkdir -p build/native/nativeCompile/ mv signal-cli build/native/nativeCompile/ chmod +x build/native/nativeCompile/signal-cli @@ -168,7 +169,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz mkdir -p client/target/release/ mv signal-cli-client client/target/release/ chmod +x client/target/release/signal-cli-client diff --git a/CHANGELOG.md b/CHANGELOG.md index eb884597..03055e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,39 @@ ### Added - `getRateLimitStatus` JSON-RPC/CLI command returning the current rate-limit state for the account (active flag, `retryAfterSeconds`, `challengeToken`, `expiresAtEpochSeconds`). Useful for admin UIs, monitoring, and clients that want to query current state without triggering a send. +## [0.14.4] - 2026-05-23 + +### Added + +- Support for a global configuration file to set system-wide defaults + +### Fixed + +- Group admins can now see profile information for users requesting to join groups. +- Storage sync with unregistered contacts fixed +- Incoming messages are validated more accurately, fixing receiving messages from new contacts + +### Improved + +- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling. + +## [0.14.3] - 2026-04-22 + +### Fixed + +- Fix sender key re-distribution on every group message (Thanks @meinecke) + +### Improved + +- Performance improvement when assigning admin role to multiple group members +- Increase disconnect timeout for websocket connections +- Release builds are now reproducible ### Changed - Send message results now surface server-advised retry time for plain rate-limit (HTTP 413) failures, not only for proof-required challenges. The `retryAfterSeconds` field in JSON-RPC `SendMessageResult` is populated whenever the server sends a `Retry-After` header. The canonical way to distinguish proof-required failures remains `token != null`. Text output includes "retry after N seconds" when known. +- Add distinct JSON-RPC error code (6) for captcha rejection (Thanks @tonycpsu) +- No longer sends busy call response to allow linked devices to accept call ## [0.14.2] - 2026-04-04 diff --git a/build.gradle.kts b/build.gradle.kts index f1e10f4d..d4f97eb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.3-SNAPSHOT" + version = "0.14.5-SNAPSHOT" } java { @@ -102,7 +102,7 @@ dependencies { implementation(libs.micronaut.json.schema.annotations) if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) { implementation(libs.micronaut.json.schema.generator) - } + } implementation(project(":libsignal-cli")) testImplementation(libs.junit.jupiter) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 53271fd0..b7c492e1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -7,11 +7,11 @@ plugins { } tasks.named>("compileKotlin").configure { - compilerOptions.jvmTarget.set(JvmTarget.JVM_24) + compilerOptions.jvmTarget.set(JvmTarget.JVM_25) } java { - targetCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_25 } repositories { diff --git a/client/Cargo.lock b/client/Cargo.lock index bd3dfd02..954b400b 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,15 +19,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -83,9 +83,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bytes" @@ -95,9 +95,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -152,15 +152,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -391,7 +391,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -399,16 +398,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "log", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -436,12 +434,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -449,9 +448,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -462,9 +461,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -476,15 +475,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -496,15 +495,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -538,9 +537,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -554,9 +553,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -567,7 +566,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -576,9 +575,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jsonrpsee" @@ -668,9 +689,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "linux-raw-sys" @@ -680,9 +701,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" @@ -698,9 +719,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[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", @@ -709,9 +730,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -757,26 +778,20 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -792,9 +807,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -815,9 +830,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -834,9 +849,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", @@ -897,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -917,9 +932,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -1026,12 +1041,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1082,12 +1097,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1132,9 +1147,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1142,9 +1157,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -1157,9 +1172,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", @@ -1202,18 +1217,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -1223,9 +1238,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -1360,14 +1375,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.6", + "webpki-root-certs 1.0.7", ] [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -1414,15 +1429,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" @@ -1456,30 +1462,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" @@ -1492,12 +1481,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" @@ -1510,12 +1493,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" @@ -1528,24 +1505,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" @@ -1558,12 +1523,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" @@ -1576,12 +1535,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" @@ -1594,12 +1547,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" @@ -1612,32 +1559,26 @@ 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" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1646,9 +1587,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1658,18 +1599,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1685,9 +1626,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1696,9 +1637,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1707,9 +1648,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index c1730192..231492be 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,12 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.4 + + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.3 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 697ab32a..ef39a50d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,12 @@ [versions] -slf4j = "2.0.17" -junit = "6.0.2" -micronaut-json-schema = "2.0.0-M6" -micronaut-core = "4.9.3" +slf4j = "2.0.18" +junit = "6.1.0" +micronaut-json-schema = "2.0.1" +micronaut-core = "5.0.0" +signal-service = "2.15.3_unofficial_147" [libraries] -bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83" +bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2" argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" @@ -18,8 +19,8 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_143" -sqlite = "org.xerial:sqlite-jdbc:3.51.2.0" +signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } +sqlite = "org.xerial:sqlite-jdbc:3.53.1.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc6..b1b8ef56 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c61a118f..df6a6ad7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index e509b2dd..aa5f10b0 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 45237064..510482e8 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin dependencies { if (libsignalClientPath == null) { - implementation(libs.signalservice) + implementation(libs.signalnetwork) } else { - implementation(libs.signalservice) { + implementation(libs.signalnetwork) { exclude(group = "org.signal", module = "libsignal-client") } implementation(files(libsignalClientPath)) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 39a4fc29..cb5a39d9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -266,7 +266,7 @@ public interface Manager extends Closeable { RecipientIdentifier.Single recipient ) throws IOException; - SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; + void sendEndSessionMessage(Set recipients) throws IOException; SendMessageResults sendMessageRequestResponse( MessageEnvelope.Sync.MessageRequestResponse.Type type, diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index c8de254b..34b2274d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -4,7 +4,6 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.MimeUtils; -import org.signal.core.models.ServiceId; import org.signal.libsignal.metadata.ProtocolException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -157,7 +156,7 @@ public record MessageEnvelope( dataMessage.getExpiresInSeconds(), dataMessage.isExpirationUpdate(), dataMessage.isViewOnce(), - dataMessage.isEndSession(), + false, dataMessage.isProfileKeyUpdate(), dataMessage.getProfileKey().isPresent(), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), @@ -1028,7 +1027,7 @@ public record MessageEnvelope( final AttachmentFileProvider fileProvider, Exception exception ) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); final var source = !envelope.isUnidentifiedSender() && serviceId != null ? recipientResolver.resolveRecipient(serviceId) : envelope.isUnidentifiedSender() && content != null diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 2396b36e..a4f037f1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -25,6 +25,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.KeyHelper; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; @@ -37,7 +38,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index afafeb24..1fefcb24 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -34,8 +34,10 @@ public class AttachmentHelper { private final SignalDependencies dependencies; private final AttachmentStore attachmentStore; + private final Context context; public AttachmentHelper(final Context context) { + this.context = context; this.dependencies = context.getDependencies(); this.attachmentStore = context.getAttachmentStore(); } @@ -92,6 +94,21 @@ public class AttachmentHelper { final boolean voiceNote ) throws AttachmentInvalidException { try { + // Reject local files that point into the signal-cli data directory + if (attachment != null && !attachment.startsWith("data:")) { + try { + final var file = new File(attachment); + final var canonical = file.getCanonicalFile(); + final var dataPath = context.getAccount().getDataPath().getCanonicalFile(); + if (canonical.toPath().startsWith(dataPath.toPath())) { + throw new AttachmentInvalidException(attachment, + new IOException("Attaching files from the signal-cli data directory is not allowed")); + } + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + final var streamDetailsAndFileName = Utils.createStreamDetails(attachment); final var streamDetails = streamDetailsAndFileName.first(); final var uploadSpec = getResumableUploadSpec(streamDetails); @@ -109,7 +126,7 @@ public class AttachmentHelper { final var streamLength = streamDetails.getLength(); final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize( streamLength)); - return dependencies.getMessageSender().getResumableUploadSpec(ciphertextLength); + return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength); } public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java index cc2d7064..ac0a2a49 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java @@ -105,6 +105,8 @@ public class CallManager implements AutoCloseable { recipientAddress, recipientId); activeCalls.put(callId, state); + dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId); + dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId); fireCallEvent(state, null); // Spawn call tunnel binary and connect control channel @@ -197,11 +199,6 @@ public class CallManager implements AutoCloseable { if (callEventListeners.isEmpty()) { logger.debug("Ignoring incoming offer for call {}: no call event listeners registered", callIdUnsigned(callId)); - - final var result = sendBusyMessage(callId, recipientId, deviceId); - if (!result.isSuccess()) { - logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId)); - } return; } @@ -701,6 +698,8 @@ public class CallManager implements AutoCloseable { private void endCall(final long callId, final String reason) { var state = activeCalls.remove(callId); + dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId); + dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId); if (state == null) return; state.state = CallInfo.State.ENDED; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 024e84fc..1c750358 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -558,16 +558,24 @@ public class GroupHelper { private void storeProfileKeysFromMembers(final DecryptedGroup group) { for (var member : group.members) { final var serviceId = ServiceId.parseOrThrow(member.aciBytes); - final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); - final var profileStore = account.getProfileStore(); - if (profileStore.getProfileKey(recipientId) != null) { - // We already have a profile key, not updating it from a non-authoritative source - continue; - } - try { - profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); - } catch (InvalidInputException ignored) { - } + storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); + } + for (var member : group.requestingMembers) { + final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); + } + } + + private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) { + final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); + final var profileStore = account.getProfileStore(); + if (profileStore.getProfileKey(recipientId) != null) { + // We already have a profile key, not updating it from a non-authoritative source + return; + } + try { + profileStore.storeProfileKey(recipientId, new ProfileKey(profileKeyBytes)); + } catch (InvalidInputException ignored) { } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index e2bc2a82..d4df661e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.signal.storageservice.storage.protos.groups.AccessControl; import org.signal.storageservice.storage.protos.groups.GroupChange; import org.signal.storageservice.storage.protos.groups.GroupChangeResponse; @@ -43,7 +44,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import java.io.IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 92014fa3..5069749c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -109,8 +109,8 @@ public final class IncomingMessageHandler { SignalServiceContent content = null; if (!envelope.isReceipt()) { account.getIdentityKeyStore().setRetryingDecryption(true); - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -140,15 +140,30 @@ public final class IncomingMessageHandler { final Manager.ReceiveMessageHandler handler ) { final var actions = new ArrayList(); + if (envelope.isPreKeySignalMessage()) { + actions.add(RefreshPreKeysAction.create()); + } SignalServiceContent content = null; Exception exception = null; - envelope.getSourceServiceId().map(ServiceId::parseOrNull) - // Store uuid if we don't have it already - // uuid in envelope is sent by server - .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId)); + if (envelope.getSourceServiceId() != null) { + // Store uuid if we don't have it already + // uuid in envelope is sent by server + account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId()); + } if (!envelope.isReceipt()) { - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); + + if (destination == account.getPni() && envelope.getSourceServiceId() == null) { + throw new InvalidMessageException( + "Got a sealed sender message to our PNI? Invalid message, ignoring."); + } + + if (envelope.getSourceServiceId() instanceof ServiceId.PNI + && envelope.getProto().type != Envelope.Type.SERVER_DELIVERY_RECEIPT) { + throw new InvalidMessageException("Got a message from a PNI that was not a SERVER_DELIVERY_RECEIPT."); + } + final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -173,7 +188,13 @@ public final class IncomingMessageHandler { logger.debug("Received invalid message from blocked contact, ignoring."); } else { var serviceId = ServiceId.parseOrNull(e.getSender()); - if (serviceId != null) { + ServiceId destination; + try { + destination = getDestination(envelope).serviceId(); + } catch (InvalidMessageException ex) { + destination = null; + } + if (serviceId != null && destination != null) { final var isSelf = sender.equals(account.getSelfRecipientId()) && e.getSenderDevice() == account.getDeviceId(); logger.debug("Received invalid message, queuing renew session action."); @@ -311,7 +332,12 @@ public final class IncomingMessageHandler { final var sender = senderDeviceAddress.recipientId(); final var senderServiceId = senderDeviceAddress.serviceId(); final var senderDeviceId = senderDeviceAddress.deviceId(); - final var destination = getDestination(envelope); + final DeviceAddress destination; + try { + destination = getDestination(envelope); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } if (account.getPni().equals(destination.serviceId)) { account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true); @@ -874,11 +900,6 @@ public final class IncomingMessageHandler { final var selfAddress = isSync ? source : destination; final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - account.getAccountData(selfAddress.serviceId()) - .getSessionStore() - .deleteAllSessions(conversationPartnerAddress.serviceId()); - } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { final var groupContext = message.getGroupContext().get(); @@ -1047,7 +1068,7 @@ public final class IncomingMessageHandler { } private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new SignalServiceAddress(serviceId); } else if (content != null) { @@ -1058,7 +1079,7 @@ public final class IncomingMessageHandler { } private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId), serviceId, @@ -1070,10 +1091,13 @@ public final class IncomingMessageHandler { } } - private DeviceAddress getDestination(SignalServiceEnvelope envelope) { + private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException { final var destination = envelope.getDestinationServiceId(); if (destination == null || destination.isUnknown()) { - return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); + throw new InvalidMessageException("Missing destination"); + } + if (!account.getAci().equals(destination) && !account.getPni().equals(destination)) { + throw new InvalidMessageException("Message not intended for this account"); } return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), destination, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java index bf7ad580..18a7b165 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -16,7 +17,6 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.List; @@ -84,7 +84,8 @@ public class PreKeyHelper { ) throws IOException { OneTimePreKeyCounts preKeyCounts; try { - preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType)); + preKeyCounts = handleResponseException(dependencies.getKeysApi() + .getAvailablePreKeyCountsSync(serviceIdType)); } catch (AuthorizationFailedException e) { logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); preKeyCounts = new OneTimePreKeyCounts(0, 0); @@ -145,7 +146,7 @@ public class PreKeyHelper { kyberPreKeyRecords); var needsReset = false; try { - NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload)); + NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload)); try { if (preKeyRecords != null) { account.addPreKeys(serviceIdType, preKeyRecords); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7906fc86..64e3a764 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.PushNetworkException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -30,7 +31,6 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index 73291d9e..31f1c04d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -9,7 +9,6 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.signal.core.models.ServiceId; import org.signal.core.models.ServiceId.ACI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,10 +149,10 @@ public class ReceiveHelper { for (final var it : batch) { SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(), it.getServerDeliveredTimestamp()); - final var recipientId = envelope1.getSourceServiceId() - .map(ServiceId::parseOrNull) - .map(s -> account.getRecipientResolver().resolveRecipient(s)) - .orElse(null); + final var sourceServiceId = envelope1.getSourceServiceId(); + final var recipientId = sourceServiceId == null + ? null + : account.getRecipientResolver().resolveRecipient(sourceServiceId); logger.trace("Storing new message from {}", recipientId); // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); @@ -238,7 +237,7 @@ public class ReceiveHelper { if (exception instanceof UntrustedIdentityException) { logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); - if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) { + if (envelope.getSourceServiceId() == null && address.aci().isPresent()) { final var recipientId = account.getRecipientResolver() .resolveRecipient(ACI.parseOrThrow(address.aci().get())); try { @@ -292,7 +291,7 @@ public class ReceiveHelper { cachedMessage.delete(); return null; } - if (envelope.getSourceServiceId().isEmpty()) { + if (envelope.getSourceServiceId() == null) { final var identifier = ((UntrustedIdentityException) exception).getSender(); final var recipientId = account.getRecipientResolver() .resolveRecipient(new RecipientAddress(identifier)); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index e84a3059..c865f38f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -11,13 +11,13 @@ import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.PNI; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.cds.CdsiV2Service; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.Collection; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index aeeb3ac5..d01bf44b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV2; +import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; @@ -17,6 +18,9 @@ import org.asamk.signal.manager.util.KeyUtils; import org.signal.core.models.storageservice.StorageKey; import org.signal.core.util.SetUtil; import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.network.service.StorageServiceService; +import org.signal.network.service.StorageServiceService.ManifestIfDifferentVersionResult; +import org.signal.network.service.StorageServiceService.WriteStorageRecordsResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; @@ -25,9 +29,6 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.StorageRecord; @@ -38,6 +39,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -211,20 +213,23 @@ public class StorageHelper { remoteOnlyRecords.size()); } - // This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated -// if (!idDifference.localOnlyIds().isEmpty()) { -// final var updated = account.getRecipientStore() -// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, -// idDifference.localOnlyIds()); -// -// if (updated > 0) { -// logger.warn( -// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", -// updated); -// } -// } -// - final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); + final var listListPair = processKnownRecords(connection, remoteOnlyRecords); + final var unknownInserts = listListPair.first(); + final var updatedStorageIds = listListPair.second(); + final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds()); + updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove); + if (!idDifference.localOnlyIds().isEmpty()) { + final var updated = account.getRecipientStore() + .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, + oldUnregisteredLocalOnlyIds); + + if (updated > 0) { + logger.warn( + "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", + updated); + } + } + final var unknownDeletes = idDifference.localOnlyIds() .stream() .filter(id -> !KNOWN_TYPES.contains(id.getType())) @@ -480,13 +485,13 @@ public class StorageHelper { private Map generateGroupV1StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); } private Map generateGroupV2StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); } private void storeManifestLocally( @@ -504,7 +509,7 @@ public class StorageHelper { final var result = dependencies.getStorageServiceRepository() .readStorageRecords(storageKey, manifest.recordIkm, storageIds); return switch (result) { - case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> { + case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> { if (decryptionError.getException() instanceof InvalidKeyException) { logger.warn("Failed to read storage records, ignoring."); yield List.of(); @@ -514,11 +519,11 @@ public class StorageHelper { throw new IOException(decryptionError.getException()); } } - case StorageServiceRepository.StorageRecordResult.NetworkError networkError -> + case StorageServiceService.StorageRecordResult.NetworkError networkError -> throw networkError.getException(); - case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError -> + case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); - case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords(); + case StorageServiceService.StorageRecordResult.Success success -> success.getRecords(); default -> throw new IllegalStateException("Unexpected value: " + result); }; } @@ -630,16 +635,17 @@ public class StorageHelper { return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch); } - private List processKnownRecords( + private Pair, List> processKnownRecords( final Connection connection, List records ) throws SQLException { final var unknownRecords = new ArrayList(); + final var processedRecords = new ArrayList(); final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor()); - final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor()); final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection); final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); + final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor()); for (final var record : records) { if (record.getProto().account != null) { @@ -662,8 +668,12 @@ public class StorageHelper { unknownRecords.add(record.getId()); } } + processedRecords.addAll(accountRecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds()); - return unknownRecords; + return new Pair<>(unknownRecords, processedRecords); } /** diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 53ab3fa4..ba2ccad1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -160,7 +160,7 @@ public class SyncHelper { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos, true, true); + var out = new DeviceContactsOutputStream(fos); for (var contactPair : account.getContactStore().getContacts()) { final var recipientId = contactPair.first(); final var contact = contactPair.second(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 4952adbd..09d8d8f4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -102,6 +102,7 @@ import org.signal.core.util.Base64; import org.signal.core.util.Hex; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.usernames.BaseUsernameException; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -118,7 +119,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Util; @@ -195,6 +195,7 @@ public class ManagerImpl implements Manager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + account.getDeviceId(), executor, sessionLock); final var avatarStore = new AvatarStore(pathConfig.avatarsPath()); @@ -1131,30 +1132,26 @@ public class ManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - - try { - return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()), - false); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } finally { - for (var recipient : recipients) { - final RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (UnregisteredRecipientException e) { - continue; - } - final var serviceId = context.getAccount() - .getRecipientAddressResolver() - .resolveRecipientAddress(recipientId) - .serviceId(); - if (serviceId.isPresent()) { - account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get()); - } + public void sendEndSessionMessage(Set recipients) throws IOException { + for (var recipient : recipients) { + final RecipientId recipientId; + try { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (UnregisteredRecipientException e) { + continue; + } + final var recipientAddress = context.getAccount() + .getRecipientAddressResolver() + .resolveRecipientAddress(recipientId); + final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore(); + final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore(); + if (recipientAddress.aci().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.aci().get()); + pniSessionStore.archiveSessions(recipientAddress.aci().get()); + } + if (recipientAddress.pni().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.pni().get()); + pniSessionStore.archiveSessions(recipientAddress.pni().get()); } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index e90bb154..1fd86c26 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + 0, null, new ReentrantSignalSessionLock()); handleResponseException(dependencies.getAccountApi() diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 77068eaa..55795714 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -5,7 +5,18 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; +import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.network.api.AttachmentApi; +import org.signal.network.api.CallingApi; +import org.signal.network.api.CdsApi; +import org.signal.network.api.CertificateApi; +import org.signal.network.api.LinkDeviceApi; +import org.signal.network.api.RateLimitChallengeApi; +import org.signal.network.api.UsernameApi; +import org.signal.network.rest.SignalRestClient; +import org.signal.network.service.CdnService; +import org.signal.network.service.StorageServiceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -14,27 +25,20 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.account.AccountApi; -import org.whispersystems.signalservice.api.attachment.AttachmentApi; -import org.whispersystems.signalservice.api.calling.CallingApi; -import org.whispersystems.signalservice.api.cds.CdsApi; -import org.whispersystems.signalservice.api.certificate.CertificateApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.keys.KeysApi; -import org.whispersystems.signalservice.api.link.LinkDeviceApi; +import org.whispersystems.signalservice.api.keys.PreKeyRepository; import org.whispersystems.signalservice.api.message.MessageApi; import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; -import org.whispersystems.signalservice.api.username.UsernameApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; @@ -61,6 +65,7 @@ public class SignalDependencies { private final String userAgent; private final CredentialsProvider credentialsProvider; private final SignalServiceDataStore dataStore; + private final int deviceId; private final ExecutorService executor; private final SignalSessionLock sessionLock; @@ -82,6 +87,11 @@ public class SignalDependencies { private KeysApi keysApi; private GroupsV2Operations groupsV2Operations; private ClientZkOperations clientZkOperations; + private ProfileService profileService; + private ProfileApi profileApi; + private CdnService cdnService; + private PreKeyRepository preKeyRepository; + private SignalRestClient signalRestClient; private PushServiceSocket pushServiceSocket; private Network libSignalNetwork; @@ -91,14 +101,13 @@ public class SignalDependencies { private SignalServiceMessageSender messageSender; private List secureValueRecovery; - private ProfileService profileService; - private ProfileApi profileApi; SignalDependencies( final ServiceEnvironmentConfig serviceEnvironmentConfig, final String userAgent, final CredentialsProvider credentialsProvider, final SignalServiceDataStore dataStore, + final int deviceId, final ExecutorService executor, final SignalSessionLock sessionLock ) { @@ -106,6 +115,7 @@ public class SignalDependencies { this.userAgent = userAgent; this.credentialsProvider = credentialsProvider; this.dataStore = dataStore; + this.deviceId = deviceId; this.executor = executor; this.sessionLock = sessionLock; } @@ -243,8 +253,8 @@ public class SignalDependencies { getPushServiceSocket())); } - public StorageServiceRepository getStorageServiceRepository() { - return new StorageServiceRepository(getStorageServiceApi()); + public StorageServiceService getStorageServiceRepository() { + return new StorageServiceService(getStorageServiceApi()); } public CertificateApi getCertificateApi() { @@ -326,12 +336,33 @@ public class SignalDependencies { () -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket())); } + private SignalRestClient getSignalRestClient() { + return getOrCreate(() -> signalRestClient, + () -> signalRestClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(), + userAgent, + credentialsProvider, + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); + } + + public CdnService getCdnService() { + return getOrCreate(() -> cdnService, + () -> cdnService = new CdnService(getSignalRestClient(), getAttachmentApi())); + } + + public PreKeyRepository getPreKeyRepository() { + final SignalProtocolAddress localProtocolAddress = credentialsProvider.getAci().toProtocolAddress(deviceId); + return getOrCreate(() -> preKeyRepository, + () -> preKeyRepository = new PreKeyRepository(getKeysApi(), + dataStore.aci(), + localProtocolAddress, + Runnable::run)); + } + public SignalServiceMessageSender getMessageSender() { return getOrCreate(() -> messageSender, () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), dataStore, sessionLock, - getAttachmentApi(), getMessageApi(), getKeysApi(), Optional.empty(), @@ -339,8 +370,7 @@ public class SignalDependencies { ServiceConfig.MAX_ENVELOPE_SIZE, ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE, () -> true, - true, - true)); + getPreKeyRepository())); } public List getSecureValueRecovery() { @@ -368,7 +398,10 @@ public class SignalDependencies { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); - final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); + final var serviceId = serviceIdType == ServiceIdType.ACI + ? credentialsProvider.getAci() + : credentialsProvider.getPni(); + final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164()); final var deviceId = credentialsProvider.getDeviceId(); return new SignalServiceCipher(address, deviceId, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 39a447ac..656b6646 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -1,8 +1,9 @@ package org.asamk.signal.manager.internal; +import org.jetbrains.annotations.NotNull; +import org.signal.network.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; @@ -94,6 +95,14 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives(); } + @Override + public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) { + if (strings.length == 0) { + return; + } + logger.info("Received alerts: {}", String.join(", ", strings)); + } + /** * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java index d25f1a72..00a284dd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java @@ -44,7 +44,8 @@ public class AttachmentStore { } public StreamDetails retrieveAttachment(final String id) throws IOException { - final var attachmentFile = new File(attachmentsPath, id); + final var safeId = sanitizeId(id); + final var attachmentFile = new File(attachmentsPath, safeId); return Utils.createStreamDetailsFromFile(attachmentFile); } @@ -61,7 +62,8 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension + ".preview"); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension + ".preview"); } private File getAttachmentFile( @@ -70,7 +72,15 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension); + } + + private static String sanitizeId(final String id) { + if (id == null) { + return ""; + } + return id.replaceAll("[^A-Za-z0-9_.-]", "_"); } private static String getAttachmentExtension(final Optional filename, final Optional contentType) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 35f600b6..c7a5c6d8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -192,6 +192,10 @@ public class SignalAccount implements Closeable { this.lock = lock; } + public File getDataPath() { + return dataPath; + } + public static SignalAccount load( File dataPath, String accountPath, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 8836a1d3..c80b3454 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -878,7 +878,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re public int removeStorageIdsFromLocalOnlyUnregisteredRecipients( final Connection connection, - final List storageIds + final Collection storageIds ) throws SQLException { final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 85b28bd7..fdc59e0c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -403,7 +403,7 @@ public class SessionStore implements SignalServiceSessionStore { } private static boolean isActive(SessionRecord record) { - return record != null && record.hasSenderChain(); + return record != null && record.hasSenderChain(0.0); } record Key(String address, int deviceId) {} diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java index 87acb991..83fd5b5f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java @@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.storage.SignalRecord; import org.whispersystems.signalservice.api.storage.StorageId; import java.sql.SQLException; +import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -24,6 +26,7 @@ abstract class DefaultStorageRecordProcessor> implemen private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class); private final Set matchedRecords = new TreeSet<>(this); + private final Set updatedStorageIds = new HashSet<>(); /** * One type of invalid remote data this handles is two records mapping to the same local data. We @@ -50,6 +53,7 @@ abstract class DefaultStorageRecordProcessor> implemen if (local.isEmpty()) { debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting."); + updatedStorageIds.add(remote.getId()); insertLocal(remote); return; } @@ -64,6 +68,7 @@ abstract class DefaultStorageRecordProcessor> implemen matchedRecords.add(local.get()); final var merged = merge(remote, local.get()); + updatedStorageIds.add(merged.getId()); if (!merged.equals(remote)) { debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote)); } @@ -75,6 +80,10 @@ abstract class DefaultStorageRecordProcessor> implemen } } + public Set getUpdatedStorageIds() { + return Collections.unmodifiableSet(updatedStorageIds); + } + private void debug(StorageId i, E record, String message) { logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message); } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java index 65d3b1d8..0061e0d9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -21,9 +21,19 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { public static File createTempFile() throws IOException { - final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp"); - tempFile.deleteOnExit(); - return tempFile; + final var prefix = "signal-cli_tmp_"; + final var suffix = ".tmp"; + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + var path = Files.createTempFile(prefix, suffix, PosixFilePermissions.asFileAttribute(perms)); + var tempFile = path.toFile(); + tempFile.deleteOnExit(); + return tempFile; + } catch (UnsupportedOperationException e) { + final var tempFile = File.createTempFile(prefix, suffix); + tempFile.deleteOnExit(); + return tempFile; + } } public static byte[] readFully(InputStream in) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java index 1908c8f5..8733deb5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java @@ -10,11 +10,11 @@ import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.helper.PinHelper; import org.signal.core.models.MasterKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException; import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.internal.push.LockedException; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index aee133ab..1871798b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -7,9 +7,9 @@ import org.signal.libsignal.net.RequestResult; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; +import org.signal.network.NetworkResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.NetworkResult; import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.util.StreamDetails; diff --git a/libsignal-version b/libsignal-version index ae02209b..6bc8ee7b 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.90.0 +0.94.1 diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 732efe43..9246e9ed 100644 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] *dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_] org.asamk.Signal. [string:] [array::] diff --git a/man/signal-cli-jsonrpc.5.adoc b/man/signal-cli-jsonrpc.5.adoc index 1c035af8..b9abb8ce 100644 --- a/man/signal-cli-jsonrpc.5.adoc +++ b/man/signal-cli-jsonrpc.5.adoc @@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc == Description diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 22b78cfd..b4dfde57 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger == Synopsis -*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] +*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] == Description @@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log Scrub possibly sensitive information from the log, like phone numbers and UUIDs. Doesn't work reliably on dbus logs with very verbose logging (`-vvv`) -*--config* CONFIG:: -Set the path, where to store the config. +*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG:: +Set the path where to store account data and local configuration. Make sure you have full read/write access to the given directory. (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) @@ -1186,10 +1186,25 @@ signal-cli -a ACCOUNT trust -a RECIPIENT == Files -The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*): `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) +=== Configuration file + +signal-cli supports a JSON-based global configuration file that provides defaults for CLI options. +Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`). + +Configuration files are read and merged in this order; later files override earlier ones: + +- `/etc/signal-cli/config.json` (system-wide defaults) +- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set) +- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`) + +When multiple configuration files are present their settings are merged; values from later files override earlier values. +Command-line options always take precedence over configuration file values. +Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults. + == Authors Maintained by AsamK , who is assisted by other open source contributors. diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 0e73ddc5..3ae4902c 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -1,13 +1,12 @@ -ARG ZULU_TAG="25.0.2-jdk@sha256:9582df6c4415d9c770eb5ff8fce426ebba53631149c9eb083ee126568d32fab3" +ARG ZULU_TAG="25-latest@sha256:8eca9375451a392bff01efe946f2e9263c50aa71a9d68423c068cc1061a41b7e" FROM docker.io/azul/zulu-openjdk:$ZULU_TAG -ENV SOURCE_DATE_EPOCH=1767225600 +ARG SOURCE_DATE_EPOCH="1776889382" +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 -ARG SNAPSHOT=20260101T000000Z -RUN echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy main" > /etc/apt/sources.list \ - && echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy universe" >> /etc/apt/sources.list -RUN apt update && apt install -y make asciidoc-base +RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ + && sed -i 's/^deb /deb [snapshot=yes] /' /etc/apt/sources.list && apt update --snapshot "$SNAPSHOT" && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] diff --git a/reproducible-builds/build.sh b/reproducible-builds/build.sh index 8a3878ce..2a46edaa 100755 --- a/reproducible-builds/build.sh +++ b/reproducible-builds/build.sh @@ -18,11 +18,8 @@ fi VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1) echo "$VERSION" >dist/VERSION -$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile . -$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile . -$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile . - # Build jar +$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build @@ -34,12 +31,14 @@ if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then fi # Build native-image +$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:native mv build/signal-cli-*-Linux-native.tar.gz dist/ # Build rust client +$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:client diff --git a/reproducible-builds/client.Containerfile b/reproducible-builds/client.Containerfile index 8a8dfa34..59d85db8 100644 --- a/reproducible-builds/client.Containerfile +++ b/reproducible-builds/client.Containerfile @@ -1,5 +1,8 @@ -FROM docker.io/rust:1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac -ENV SOURCE_DATE_EPOCH=1767225600 +ARG RUST_TAG="1-slim@sha256:715efd1ccdc4a63bd6a6e2f54387fff73f904b70e610d41b4d9d74ff38e13ad3" + +FROM docker.io/rust:$RUST_TAG +ARG SOURCE_DATE_EPOCH="1776889382" +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/reproducible-builds/native.Containerfile b/reproducible-builds/native.Containerfile index 47cd5ce3..a242538d 100644 --- a/reproducible-builds/native.Containerfile +++ b/reproducible-builds/native.Containerfile @@ -1,5 +1,8 @@ -FROM container-registry.oracle.com/graalvm/native-image:25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8 -ENV SOURCE_DATE_EPOCH=1767225600 +ARG GRAALVM_TAG="25@sha256:38f835ccb37d4a106c37376a98e8713999077a8c8173d9876505f77da438332c" + +FROM container-registry.oracle.com/graalvm/native-image:$GRAALVM_TAG +ARG SOURCE_DATE_EPOCH="1776889382" +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/reproducible-builds/update-pinned-container-versions.sh b/reproducible-builds/update-pinned-container-versions.sh new file mode 100755 index 00000000..f03c4333 --- /dev/null +++ b/reproducible-builds/update-pinned-container-versions.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../" +cd "$ROOT_DIR" + +if command -v podman >/dev/null; then + ENGINE=podman +elif command -v docker >/dev/null; then + ENGINE=docker +else + echo "error: neither podman nor docker is available" >&2 + exit 1 +fi + +resolve_digest() { + local image_ref="$1" + "$ENGINE" pull "$image_ref" >/dev/null + "$ENGINE" image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image_ref" \ + | grep -m1 -E '@sha256:[0-9a-f]{64}$' \ + | sed -E 's|.*(@sha256:[0-9a-f]{64})$|\1|' +} + +update_arg_tag() { + local file="$1" + local arg_name="$2" + local image_prefix="$3" + local current + current="$(sed -n "s/^ARG ${arg_name}=\"\([^\"]*\)\"$/\\1/p" "$file")" + if [[ -z "$current" ]]; then + echo "error: could not find ARG ${arg_name} in $file" >&2 + exit 1 + fi + local tag + tag="${current%@*}" + local digest + digest="$(resolve_digest "${image_prefix}${tag}")" + sed -i -E "s|^ARG ${arg_name}=\"[^\"]+\"$|ARG ${arg_name}=\"${tag}${digest}\"|" "$file" + echo "updated $file -> ${tag}${digest}" +} + +update_source_date_epoch() { + local file="$1" + local current_timestamp + current_timestamp="$(date +%s)" + sed -i -E "s|^ARG SOURCE_DATE_EPOCH=\"[^\"]+\"$|ARG SOURCE_DATE_EPOCH=\"${current_timestamp}\"|" "$file" + echo "updated $file SOURCE_DATE_EPOCH -> ${current_timestamp}" +} + +update_arg_tag reproducible-builds/build.Containerfile ZULU_TAG docker.io/azul/zulu-openjdk: +update_arg_tag reproducible-builds/native.Containerfile GRAALVM_TAG container-registry.oracle.com/graalvm/native-image: +update_arg_tag reproducible-builds/client.Containerfile RUST_TAG docker.io/rust: + +update_source_date_epoch reproducible-builds/build.Containerfile +update_source_date_epoch reproducible-builds/native.Containerfile +update_source_date_epoch reproducible-builds/client.Containerfile diff --git a/run_tests.sh b/run_tests.sh index 81d2c007..1a1c11e3 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -38,6 +38,13 @@ else SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" fi +# Prefer line-buffered output for external commands when available +if command -v stdbuf >/dev/null 2>&1; then + STD_BUF="stdbuf -oL -eL --" +else + STD_BUF="" +fi + run() { # To update graalvm config, set GRAALVM_HOME, e.g: # export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm @@ -48,11 +55,23 @@ run() { set -x if [ "$JSON_RPC" -eq 1 ]; then - "$SIGNAL_CLI" $@ + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" $@ + else + "$SIGNAL_CLI" $@ + fi elif [ "$DBUS" -eq 1 ]; then - "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + else + "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + fi else - "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + else + "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + fi fi set +x } @@ -98,9 +117,10 @@ link() { rm -f "$LINK_CODE_FILE" mkfifo "$LINK_CODE_FILE" run_linked link -n "test-device" >"$LINK_CODE_FILE" & - read LINK_CODE <"$LINK_CODE_FILE" + LINK_PID=$! + read -r LINK_CODE <"$LINK_CODE_FILE" run_main -a "$NUMBER" addDevice --uri "$LINK_CODE" - wait + wait $LINK_PID run_linked -a "$NUMBER" send --note-to-self -m hi run_main -a "$NUMBER" receive run_linked -a "$NUMBER" receive @@ -180,6 +200,7 @@ run_main -a "$NUMBER_2" updateContact "$NUMBER_1" -n NUMBER_1 -e 10 run_main -a "$NUMBER_2" block "$NUMBER_1" run_main -a "$NUMBER_2" unblock "$NUMBER_1" run_main -a "$NUMBER_2" listContacts +run_main -a "$NUMBER_2" listContacts "$NUMBER_1" run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi run_main -a "$NUMBER_2" receive @@ -207,6 +228,7 @@ run_main -a "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2" run_main -a "$NUMBER_1" listGroups -d run_main -a "$NUMBER_1" --output=json listGroups -d run_main -a "$NUMBER_2" receive +run_main -a "$NUMBER_2" listGroups -g "$GROUP_ID" run_main -a "$NUMBER_2" quitGroup -g "$GROUP_ID" run_main -a "$NUMBER_2" listGroups -d run_main -a "$NUMBER_2" --output=json listGroups -d @@ -228,6 +250,7 @@ for OUTPUT in "plain-text" "json"; do run_main -a "$NUMBER_2" --output="$OUTPUT" receive run_main -a "$NUMBER_1" --output="$OUTPUT" receive run_main -a "$NUMBER_1" --output="$OUTPUT" send -e "$NUMBER_2" + run_main -a "$NUMBER_1" --output="$OUTPUT" send "$NUMBER_2" -m test run_main -a "$NUMBER_2" --output="$OUTPUT" receive done @@ -235,8 +258,8 @@ done run_main -a "$NUMBER_1" updateProfile --given-name=GIVEN --family-name=FAMILY --about=ABOUT --about-emoji=EMOJI --avatar=LICENSE --mobile-coin-address="YWJjCg==" ## Provisioning -link "$NUMBER_1" -link "$NUMBER_2" +link "$NUMBER_1" || true +link "$NUMBER_2" || true run_main -a "$NUMBER_1" listDevices run_linked -a "$NUMBER_1" sendSyncRequest run_main -a "$NUMBER_1" sendContacts diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bcf3c605..fb940602 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -46,7 +46,9 @@ public class App { private final Namespace ns; - static ArgumentParser buildArgumentParser() { + static ArgumentParser buildArgumentParser(GlobalConfig config) { + final var cfg = config == null ? GlobalConfig.DEFAULT : config; + var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() @@ -57,47 +59,60 @@ public class App { parser.addArgument("--version").help("Show package version.").action(Arguments.version()); parser.addArgument("-v", "--verbose") .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.") - .action(Arguments.count()); + .action(Arguments.count()) + .setDefault(cfg.verbose() == null ? 0 : cfg.verbose()); parser.addArgument("--log-file") .type(File.class) - .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file."); + .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.") + .setDefault(cfg.logFile() == null ? null : new File(cfg.logFile())); parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) - .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs."); - parser.addArgument("-c", "--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.") + .setDefault(cfg.scrubLog() == null ? false : cfg.scrubLog()); + parser.addArgument("-d", "--data-dir", "-c", "--config") + .help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).") + .setDefault(cfg.dataDir()); parser.addArgument("-a", "--account", "-u", "--username") .help("Specify your phone number, that will be your identifier."); var mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus") + .dest("global-dbus") + .help("Make request via user dbus.") + .action(Arguments.storeTrue()) + .setDefault(cfg.dbus() == null ? false : cfg.dbus()); mut.addArgument("--dbus-system") .dest("global-dbus-system") .help("Make request via system dbus.") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.dbusSystem() == null ? false : cfg.dbusSystem()); parser.addArgument("--bus-name") .dest("global-bus-name") - .setDefault(DbusConfig.getBusname()) + .setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname()) .help("Specify the D-Bus bus name to connect to."); parser.addArgument("-o", "--output") .help("Choose to output in plain text or JSON") - .type(Arguments.enumStringType(OutputType.class)); + .type(Arguments.enumStringType(OutputType.class)) + .setDefault(cfg.output() == null ? null : cfg.output()); parser.addArgument("--service-environment") .help("Choose the server environment to use.") .type(Arguments.enumStringType(ServiceEnvironmentCli.class)) - .setDefault(ServiceEnvironmentCli.LIVE); + .setDefault(cfg.serviceEnvironment() != null ? cfg.serviceEnvironment() : ServiceEnvironmentCli.LIVE); parser.addArgument("--trust-new-identities") .help("Choose when to trust new identities.") .type(Arguments.enumStringType(TrustNewIdentityCli.class)) - .setDefault(TrustNewIdentityCli.ON_FIRST_USE); + .setDefault(cfg.trustNewIdentities() != null + ? cfg.trustNewIdentities() + : TrustNewIdentityCli.ON_FIRST_USE); parser.addArgument("--disable-send-log") .help("Disable message send log (for resending messages that recipient couldn't decrypt)") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.disableSendLog() != null ? cfg.disableSendLog() : false); parser.epilog( "The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand."); @@ -219,12 +234,12 @@ public class App { } private SignalAccountFiles loadSignalAccountFiles() throws IOErrorException { - final File configPath; - final var config = ns.getString("config"); - if (config != null) { - configPath = new File(config); + final File dataPath; + final var dataDir = ns.getString("data-dir"); + if (dataDir != null) { + dataPath = new File(dataDir); } else { - configPath = getDefaultConfigPath(); + dataPath = getDefaultDataPath(); } final var serviceEnvironmentCli = ns.get("service-environment"); @@ -240,7 +255,7 @@ public class App { final var disableSendLog = Boolean.TRUE.equals(ns.getBoolean("disable-send-log")); try { - return new SignalAccountFiles(configPath, + return new SignalAccountFiles(dataPath, serviceEnvironment, BaseConfig.USER_AGENT, new Settings(trustNewIdentity, disableSendLog)); @@ -339,7 +354,7 @@ public class App { /** * @return the default data directory to be used by signal-cli. */ - private static File getDefaultConfigPath() { + private static File getDefaultDataPath() { return new File(IOUtils.getDataHomeDir(), "signal-cli"); } } diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index 022a22f1..da2e5422 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.6.1"); + .orElse("Signal-Android/8.12.1"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/java/org/asamk/signal/ConfigLoader.java b/src/main/java/org/asamk/signal/ConfigLoader.java new file mode 100644 index 00000000..861de2e0 --- /dev/null +++ b/src/main/java/org/asamk/signal/ConfigLoader.java @@ -0,0 +1,81 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.asamk.signal.commands.exceptions.UserErrorException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Loads and merges configuration files. Merge order (later files override earlier): + * - /etc/signal-cli/config.json + * - file pointed to by SIGNAL_CLI_CONFIG (if set) + * - $XDG_CONFIG_HOME/signal-cli/config.json or $HOME/.config/signal-cli/config.json + */ +public final class ConfigLoader { + + private ConfigLoader() { + } + + public static GlobalConfig load() throws UserErrorException { + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode merged = mapper.createObjectNode(); + + // System config + addIfExists(merged, mapper, Paths.get("/etc/signal-cli/config.json")); + + // User config via env (if set) else XDG or ~/.config + final String env = System.getenv("SIGNAL_CLI_CONFIG"); + if (env != null && !env.isEmpty()) { + addIfExists(merged, mapper, Paths.get(env)); + } else { + final String xdg = System.getenv("XDG_CONFIG_HOME"); + if (xdg != null && !xdg.isEmpty()) { + addIfExists(merged, mapper, Paths.get(xdg, "signal-cli", "config.json")); + } else { + addIfExists(merged, + mapper, + Paths.get(System.getProperty("user.home"), ".config", "signal-cli", "config.json")); + } + } + + try { + if (merged.isEmpty()) { + return GlobalConfig.DEFAULT; + } + return mapper.treeToValue(merged, GlobalConfig.class); + } catch (Exception e) { + throw new UserErrorException("Failed to parse configuration file(s): " + e.getMessage(), e); + } + } + + private static void addIfExists(ObjectNode merged, ObjectMapper mapper, Path p) throws UserErrorException { + if (p == null) return; + try { + if (Files.exists(p)) { + final JsonNode node = mapper.readTree(p.toFile()); + merge(merged, node); + } + } catch (IOException e) { + throw new UserErrorException("Failed to load config from " + p + ": " + e.getMessage(), e); + } + } + + private static void merge(ObjectNode target, JsonNode source) { + source.properties().forEach(entry -> { + final String name = entry.getKey(); + final JsonNode value = entry.getValue(); + final JsonNode existing = target.get(name); + if (existing != null && existing.isObject() && value.isObject()) { + merge((ObjectNode) existing, value); + } else { + target.set(name, value); + } + }); + } +} diff --git a/src/main/java/org/asamk/signal/GlobalConfig.java b/src/main/java/org/asamk/signal/GlobalConfig.java new file mode 100644 index 00000000..1205df7c --- /dev/null +++ b/src/main/java/org/asamk/signal/GlobalConfig.java @@ -0,0 +1,36 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GlobalConfig( + @JsonProperty("verbose") Integer verbose, + @JsonProperty("logFile") String logFile, + @JsonProperty("scrubLog") Boolean scrubLog, + @JsonProperty("dataDir") String dataDir, + @JsonProperty("busName") String busName, + @JsonProperty("dbus") Boolean dbus, + @JsonProperty("dbusSystem") Boolean dbusSystem, + @JsonProperty("output") OutputType output, + @JsonProperty("serviceEnvironment") ServiceEnvironmentCli serviceEnvironment, + @JsonProperty("trustNewIdentities") TrustNewIdentityCli trustNewIdentities, + @JsonProperty("disableSendLog") Boolean disableSendLog, + @JsonProperty("account") String account +) { + + public static final GlobalConfig DEFAULT = new GlobalConfig(null, + null, + null, + null, + null, + null, + null, + null, + ServiceEnvironmentCli.LIVE, + TrustNewIdentityCli.ON_FIRST_USE, + null, + null); + + public static GlobalConfig empty() { + return new GlobalConfig(null, null, null, null, null, null, null, null, null, null, null, null); + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 568f58c6..0c9bddc0 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,27 +40,32 @@ import java.security.Security; public class Main { - public static void main(String[] args) { + static void main(String[] args) { // enable unlimited strength crypto via Policy, supported on relevant JREs Security.setProperty("crypto.policy", "unlimited"); installSecurityProviderWorkaround(); + // Load global config early so we can use its values as parser defaults + final GlobalConfig globalConfig; + try { + globalConfig = ConfigLoader.load(); + } catch (UserErrorException e) { + System.exit(handleCommandException(e, null)); + return; + } + // Configuring the logger needs to happen before any logger is initialized - final var loggingConfig = parseLoggingConfig(args); + final var loggingConfig = parseLoggingConfig(args, globalConfig); configureLogging(loggingConfig); - final var parser = App.buildArgumentParser(); + final var parser = App.buildArgumentParser(globalConfig); final var ns = parser.parseArgsOrFail(args); int status = 0; try { new App(ns).init(); } catch (CommandException e) { - System.err.println(e.getMessage()); - if (loggingConfig.verboseLevel > 0 && e.getCause() != null) { - e.getCause().printStackTrace(System.err); - } - status = getStatusForError(e); + status = handleCommandException(e, loggingConfig); } catch (Throwable e) { e.printStackTrace(System.err); status = 2; @@ -69,16 +74,27 @@ public class Main { System.exit(status); } + private static int handleCommandException(final CommandException e, final LoggingConfig loggingConfig) { + System.err.println(e.getMessage()); + if (loggingConfig != null && loggingConfig.verboseLevel > 0 && e.getCause() != null) { + e.getCause().printStackTrace(System.err); + } + return getStatusForError(e); + } + private static void installSecurityProviderWorkaround() { // Register our own security provider Security.insertProviderAt(new SecurityProvider(), 1); Security.addProvider(new BouncyCastleProvider()); } - private static LoggingConfig parseLoggingConfig(final String[] args) { - final var nsLog = parseArgs(args); + private static LoggingConfig parseLoggingConfig(final String[] args, final GlobalConfig config) { + final var nsLog = parseArgs(args, config); if (nsLog == null) { - return new LoggingConfig(0, null, false); + final var verbose = config != null && config.verbose() != null ? config.verbose() : 0; + final var logFile = config != null && config.logFile() != null ? new File(config.logFile()) : null; + final var scrubLog = config != null && Boolean.TRUE.equals(config.scrubLog()); + return new LoggingConfig(verbose, logFile, scrubLog); } final var verboseLevel = nsLog.getInt("verbose"); @@ -90,14 +106,20 @@ public class Main { /** * This method only parses commandline args relevant for logging configuration. */ - private static Namespace parseArgs(String[] args) { + private static Namespace parseArgs(String[] args, final GlobalConfig config) { var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() .defaultHelp(false); - parser.addArgument("-v", "--verbose").action(Arguments.count()); - parser.addArgument("--log-file").type(File.class); - parser.addArgument("--scrub-log").action(Arguments.storeTrue()); + parser.addArgument("-v", "--verbose") + .action(Arguments.count()) + .setDefault(config == null || config.verbose() == null ? 0 : config.verbose()); + parser.addArgument("--log-file") + .type(File.class) + .setDefault(config == null || config.logFile() == null ? null : new File(config.logFile())); + parser.addArgument("--scrub-log") + .action(Arguments.storeTrue()) + .setDefault(config == null || config.scrubLog() == null ? false : config.scrubLog()); try { return parser.parseKnownArgs(args, null); @@ -124,12 +146,12 @@ public class Main { private static int getStatusForError(final CommandException e) { return switch (e) { - case UserErrorException userErrorException -> 1; - case UnexpectedErrorException unexpectedErrorException -> 2; - case IOErrorException ioErrorException -> 3; - case UntrustedKeyErrorException untrustedKeyErrorException -> 4; - case RateLimitErrorException rateLimitErrorException -> 5; - case CaptchaRejectedErrorException captchaRejectedErrorException -> 6; + case UserErrorException _ -> 1; + case UnexpectedErrorException _ -> 2; + case IOErrorException _ -> 3; + case UntrustedKeyErrorException _ -> 4; + case RateLimitErrorException _ -> 5; + case CaptchaRejectedErrorException _ -> 6; case null -> 2; }; } diff --git a/src/main/java/org/asamk/signal/OutputType.java b/src/main/java/org/asamk/signal/OutputType.java index 383d635f..2da6b5db 100644 --- a/src/main/java/org/asamk/signal/OutputType.java +++ b/src/main/java/org/asamk/signal/OutputType.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum OutputType { PLAIN_TEXT { @Override @@ -12,5 +14,16 @@ public enum OutputType { public String toString() { return "json"; } - }, + }; + + @JsonCreator + public static OutputType fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "plaintext" -> PLAIN_TEXT; + case "json" -> JSON; + default -> throw new IllegalArgumentException("Invalid output type: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java index 9010f3f8..f61a6833 100644 --- a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java +++ b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum ServiceEnvironmentCli { LIVE { @Override @@ -12,5 +14,16 @@ public enum ServiceEnvironmentCli { public String toString() { return "staging"; } - }, + }; + + @JsonCreator + public static ServiceEnvironmentCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase(); + return switch (norm) { + case "live" -> LIVE; + case "staging" -> STAGING; + default -> throw new IllegalArgumentException("Invalid service-environment: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java index 5cc36bbd..665ab90b 100644 --- a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum TrustNewIdentityCli { ALWAYS { @Override @@ -18,5 +20,17 @@ public enum TrustNewIdentityCli { public String toString() { return "never"; } - }, + }; + + @JsonCreator + public static TrustNewIdentityCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "always" -> ALWAYS; + case "onfirstuse" -> ON_FIRST_USE; + case "never" -> NEVER; + default -> throw new IllegalArgumentException("Invalid trust-new-identities: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index aa4bb957..c6ef5077 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -144,8 +144,7 @@ public class SendCommand implements JsonRpcLocalCommand { } try { - final var results = m.sendEndSessionMessage(singleRecipients); - outputResult(outputWriter, results); + m.sendEndSessionMessage(singleRecipients); return; } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 33ffb3cc..f719cfed 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -549,9 +549,8 @@ public class DbusManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { + public void sendEndSessionMessage(final Set recipients) throws IOException { signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); - return new SendMessageResults(0, Map.of()); } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 20069b9a..a9983caa 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -430,8 +430,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); - checkSendMessageResults(results); + m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 1e62c3de..551c5b04 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -19,8 +19,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,12 +40,14 @@ public class HttpServerHandler implements AutoCloseable { private final Manager m; private HttpServer server; private final AtomicBoolean shutdown = new AtomicBoolean(false); + private final Set allowedHosts; public HttpServerHandler(final InetSocketAddress address, final Manager m) { this.address = address; commandHandler = new SignalJsonRpcCommandHandler(m, Commands::getCommand); this.c = null; this.m = m; + this.allowedHosts = buildAllowedHosts(address); } public HttpServerHandler(final InetSocketAddress address, final MultiAccountManager c) { @@ -50,6 +55,7 @@ public class HttpServerHandler implements AutoCloseable { commandHandler = new SignalJsonRpcCommandHandler(c, Commands::getCommand); this.c = c; this.m = null; + this.allowedHosts = buildAllowedHosts(address); } public void init() throws IOException { @@ -67,6 +73,7 @@ public class HttpServerHandler implements AutoCloseable { server.start(); logger.info("Started HTTP server on {}", address); + logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); } @Override @@ -99,6 +106,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected RPC request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/rpc".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -146,6 +159,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected Events request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/events".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -169,6 +188,12 @@ public class HttpServerHandler implements AutoCloseable { httpExchange.sendResponseHeaders(200, 0); final var sender = new ServerSentEventSender(httpExchange.getResponseBody()); + // Flush HTTP response headers to the client immediately. + // Without this, the JVM HttpServer buffers everything until a later write + // in the keep-alive loop (15 s), causing clients with shorter timeouts + // (e.g. 10 s) to abort before receiving the initial response. + httpExchange.getResponseBody().flush(); + final var shouldStop = new AtomicBoolean(false); final var handlers = subscribeReceiveHandlers(managers, sender, () -> { shouldStop.set(true); @@ -267,4 +292,59 @@ public class HttpServerHandler implements AutoCloseable { void call(); } + + private Set buildAllowedHosts(final InetSocketAddress address) { + final var s = new HashSet(); + final var host = address == null ? null : address.getHostString(); + if (host != null && !host.isEmpty()) { + s.add(host.toLowerCase(Locale.ROOT)); + } + s.add("localhost"); + s.add("127.0.0.1"); + s.add("::1"); + return s; + } + + private boolean isHostAllowed(final HttpExchange httpExchange) { + final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host"); + if (hostHeader == null || hostHeader.isEmpty()) { + return false; + } + + String hostPart = hostHeader; + String portPart = null; + if (hostHeader.startsWith("[")) { + final var idx = hostHeader.indexOf(']'); + if (idx == -1) return false; + hostPart = hostHeader.substring(1, idx); + if (hostHeader.length() > idx + 1 && hostHeader.charAt(idx + 1) == ':') { + portPart = hostHeader.substring(idx + 2); + } + } else { + final var colon = hostHeader.lastIndexOf(':'); + if (colon != -1) { + final var possiblePort = hostHeader.substring(colon + 1); + if (possiblePort.chars().allMatch(Character::isDigit)) { + hostPart = hostHeader.substring(0, colon); + portPart = possiblePort; + } + } + } + + hostPart = hostPart.toLowerCase(Locale.ROOT); + if (!allowedHosts.contains(hostPart)) { + return false; + } + + if (portPart != null) { + try { + final var port = Integer.parseInt(portPart); + if (port != address.getPort()) return false; + } catch (NumberFormatException e) { + return false; + } + } + + return true; + } } diff --git a/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java b/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java index 14ff0076..166c822a 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java +++ b/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java @@ -25,12 +25,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import java.util.stream.Collectors; public class SignalJsonRpcDispatcherHandler { @@ -41,8 +43,8 @@ public class SignalJsonRpcDispatcherHandler { private final JsonRpcReader jsonRpcReader; private final boolean noReceiveOnStart; - private final Map>> receiveHandlers = new HashMap<>(); - private final Map>> callEventHandlers = new HashMap<>(); + private final Map>> receiveHandlers = new HashMap<>(); + private final Map>> callEventHandlers = new HashMap<>(); private SignalJsonRpcCommandHandler commandHandler; public SignalJsonRpcDispatcherHandler( @@ -91,7 +93,9 @@ public class SignalJsonRpcDispatcherHandler { private int subscribeCallEvents(final Collection managers) { final var subscriptionId = nextSubscriptionId.getAndIncrement(); - final var listeners = managers.stream().map(m -> createCallEventHandler(m, subscriptionId)).toList(); + final var listeners = managers.stream() + .map(m -> createCallEventHandler(m, subscriptionId)) + .collect(Collectors.toCollection(ArrayList::new)); callEventHandlers.put(subscriptionId, listeners); return subscriptionId; } @@ -146,7 +150,7 @@ public class SignalJsonRpcDispatcherHandler { final var subscriptionId = nextSubscriptionId.getAndIncrement(); final var handlers = managers.stream() .map(m -> createReceiveHandler(m, subscriptionId, internalSubscription)) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); receiveHandlers.put(subscriptionId, handlers); return subscriptionId; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index f70e705d..dff20afe 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1317,6 +1317,14 @@ } ] }, + { + "type": "java.util.concurrent.CopyOnWriteArrayList", + "fields": [ + { + "name": "lock" + } + ] + }, { "type": "java.util.concurrent.ForkJoinTask", "fields": [ @@ -1564,6 +1572,9 @@ } ] }, + { + "type": "kotlin.reflect.jvm.internal.impl.km.jvm.internal.JvmMetadataExtensions" + }, { "type": "kotlin.reflect.jvm.internal.impl.load.java.ErasedOverridabilityCondition" }, @@ -1575,42 +1586,13 @@ }, { "type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter", - "allPublicFields": true, - "fields": [ - { - "name": "ALL" - }, - { - "name": "CALLABLES" - }, - { - "name": "CLASSIFIERS" - }, - { - "name": "Companion" - }, - { - "name": "FUNCTIONS" - }, - { - "name": "NON_SINGLETON_CLASSIFIERS" - }, - { - "name": "PACKAGES" - }, - { - "name": "SINGLETON_CLASSIFIERS" - }, - { - "name": "TYPE_ALIASES" - }, - { - "name": "VALUES" - }, - { - "name": "VARIABLES" - } - ] + "allPublicFields": true + }, + { + "type": "kotlinx.coroutines.AwaitAll" + }, + { + "type": "kotlinx.coroutines.AwaitAll$AwaitAllNode" }, { "type": "kotlinx.coroutines.CancellableContinuationImpl", @@ -1626,6 +1608,9 @@ } ] }, + { + "type": "kotlinx.coroutines.CancelledContinuation" + }, { "type": "kotlinx.coroutines.CompletedExceptionally", "fields": [ @@ -1681,6 +1666,9 @@ } ] }, + { + "type": "kotlinx.coroutines.internal.LimitedDispatcher" + }, { "type": "kotlinx.coroutines.internal.LockFreeLinkedListNode", "fields": [ @@ -1714,6 +1702,9 @@ } ] }, + { + "type": "kotlinx.coroutines.internal.ThreadSafeHeap" + }, { "type": "kotlinx.coroutines.scheduling.CoroutineScheduler", "fields": [ @@ -1728,6 +1719,12 @@ } ] }, + { + "type": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker" + }, + { + "type": "kotlinx.coroutines.scheduling.WorkQueue" + }, { "type": "libcore.io.Memory" }, @@ -1961,6 +1958,28 @@ { "type": "org.asamk.SignalControl.Error.InvalidNumber" }, + { + "type": "org.asamk.signal.GlobalConfig", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Integer", + "java.lang.String", + "java.lang.Boolean", + "java.lang.String", + "java.lang.String", + "java.lang.Boolean", + "java.lang.Boolean", + "org.asamk.signal.OutputType", + "org.asamk.signal.ServiceEnvironmentCli", + "org.asamk.signal.TrustNewIdentityCli", + "java.lang.Boolean", + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.Main", "jniAccessible": true, @@ -1977,6 +1996,31 @@ } ] }, + { + "type": "org.asamk.signal.OutputType" + }, + { + "type": "org.asamk.signal.ServiceEnvironmentCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "org.asamk.signal.TrustNewIdentityCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.commands.AcceptCallCommand$JsonCallInfo", "allDeclaredFields": true, @@ -4898,6 +4942,15 @@ } ] }, + { + "type": "org.bouncycastle.jcajce.provider.kdf.PBKDF2$Mappings", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.bouncycastle.jcajce.provider.kdf.SCRYPT$Mappings", "methods": [ @@ -6461,6 +6514,68 @@ "type": "org.signal.libsignal.zkgroup.profiles.ProfileKey", "allDeclaredFields": true }, + { + "type": "org.signal.network.api.SenderCertificate", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCertificate", + "parameterTypes": [ + "byte[]" + ] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArrayDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" + }, + { + "type": "org.signal.network.api.SubmitRecaptchaChallengePayload", + "methods": [ + { + "name": "getCaptcha", + "parameterTypes": [] + }, + { + "name": "getToken", + "parameterTypes": [] + }, + { + "name": "getType", + "parameterTypes": [] + } + ] + }, + { + "type": "org.signal.network.util.JsonUtil$IdentityKeyDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "type": "org.signal.network.util.JsonUtil$ServiceIdDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.signal.storageservice.storage.protos.groups.AccessControl", "fields": [ @@ -10115,6 +10230,9 @@ { "glob": "META-INF/services/java.util.spi.ResourceBundleControlProvider" }, + { + "glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.km.internal.extensions.MetadataExtensions" + }, { "glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition" }, diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java new file mode 100644 index 00000000..d62c5220 --- /dev/null +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -0,0 +1,536 @@ +package org.asamk.signal.http; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.CallInfo; +import org.asamk.signal.manager.api.CallOffer; +import org.asamk.signal.manager.api.Configuration; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.DeviceLinkUrl; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.GroupInviteLinkUrl; +import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.IdentityVerificationCode; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.MessageEnvelope; +import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.api.ReceiveConfig; +import org.asamk.signal.manager.api.Recipient; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResult; +import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.StickerPack; +import org.asamk.signal.manager.api.StickerPackId; +import org.asamk.signal.manager.api.StickerPackUrl; +import org.asamk.signal.manager.api.TurnServer; +import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; +import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.manager.api.UsernameLinkUrl; +import org.asamk.signal.manager.api.UsernameStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.URI; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Regression test for the SSE initial-flush bug: + * HttpServerHandler used to flush the initial SSE response only after a later + * write in the 15-second keep-alive loop, meaning the HTTP response headers + * were not flushed to the client until then. + * Clients with a shorter connection timeout (e.g. 10 s) would time out before + * receiving the initial response. + * + * This test verifies that the endpoint returns HTTP 200 within 2 seconds of + * connecting to GET /api/v1/events. + */ +class SseInitialFlushTest { + + private HttpServerHandler handler; + private int port; + + /** Finds a free local port. */ + private static int freePort() throws Exception { + try (var ss = new ServerSocket(0)) { + ss.setReuseAddress(true); + return ss.getLocalPort(); + } + } + + @BeforeEach + void setUp() throws Exception { + port = freePort(); + handler = new HttpServerHandler(new InetSocketAddress("127.0.0.1", port), new MinimalStubManager()); + handler.init(); + } + + @AfterEach + void tearDown() { + if (handler != null) { + handler.close(); + } + } + + /** + * The SSE endpoint MUST flush the initial HTTP response immediately upon + * connection, before the 15-second keep-alive loop fires. A read timeout of + * 2 000 ms is used — well below the 15-second wait interval but generous + * enough to survive any CI scheduling jitter. + */ + @Test + void sseEndpointReturnsHeadersWithinTwoSeconds() { + assertDoesNotThrow(() -> { + var url = new URI("http", null, "127.0.0.1", port, "/api/v1/events", null, null).toURL(); + var conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Accept", "text/event-stream"); + conn.setReadTimeout(2_000); // 2 s — fails before fix (15 s flush), passes after + conn.setConnectTimeout(2_000); + try { + conn.connect(); + assertEquals(200, conn.getResponseCode()); + } finally { + conn.disconnect(); + } + }, "SSE endpoint did not return the initial response within 2 seconds"); + } + + // ------------------------------------------------------------------------- + // Minimal Manager stub — only receive-handler methods need real behaviour; + // everything else is a no-op stub. + // ------------------------------------------------------------------------- + + private static final class MinimalStubManager implements Manager { + + @Override + public String getSelfNumber() { + return "+10000000000"; + } + + @Override + public void addReceiveHandler(ReceiveMessageHandler handler, boolean isWeakListener) { + // no-op + } + + @Override + public void removeReceiveHandler(ReceiveMessageHandler handler) { + // no-op + } + + @Override + public boolean isReceiving() { + return false; + } + + @Override + public void receiveMessages(Optional timeout, Optional maxMessages, ReceiveMessageHandler handler) { + } + + @Override + public void stopReceiveMessages() { + } + + @Override + public void setReceiveConfig(ReceiveConfig receiveConfig) { + } + + @Override + public Map getUserStatus(Set numbers) { + return Map.of(); + } + + @Override + public Map getUsernameStatus(Set usernames) { + return Map.of(); + } + + @Override + public void updateAccountAttributes(String deviceName, Boolean unidentifiedDeliveryIndicators, Boolean discoverableByNumber, Boolean numberSharing) { + } + + @Override + public Configuration getConfiguration() { + return null; + } + + @Override + public void updateConfiguration(Configuration configuration) { + } + + @Override + public void updateProfile(UpdateProfile updateProfile) { + } + + @Override + public String getUsername() { + return null; + } + + @Override + public UsernameLinkUrl getUsernameLink() { + return null; + } + + @Override + public void setUsername(String username) { + } + + @Override + public void deleteUsername() { + } + + @Override + public void startChangeNumber(String newNumber, boolean voiceVerification, String captcha) { + } + + @Override + public void finishChangeNumber(String newNumber, String verificationCode, String pin) { + } + + @Override + public void unregister() { + } + + @Override + public void deleteAccount() { + } + + @Override + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) { + } + + @Override + public List getLinkedDevices() { + return List.of(); + } + + @Override + public void updateLinkedDevice(int deviceId, String name) { + } + + @Override + public void removeLinkedDevices(int deviceId) { + } + + @Override + public void addDeviceLink(DeviceLinkUrl deviceLinkUrl) { + } + + @Override + public void setRegistrationLockPin(Optional pin) { + } + + @Override + public List getGroups() { + return List.of(); + } + + @Override + public List getGroups(Collection groupIds) { + return List.of(); + } + + @Override + public SendGroupMessageResults quitGroup(GroupId groupId, Set administrators) { + return null; + } + + @Override + public void deleteGroup(GroupId groupId) { + } + + @Override + public Pair createGroup(String name, Set members, String avatarFile) { + return null; + } + + @Override + public SendGroupMessageResults updateGroup(GroupId groupId, UpdateGroup updateGroup) { + return null; + } + + @Override + public Pair joinGroup(GroupInviteLinkUrl inviteLinkUrl) { + return null; + } + + @Override + public SendMessageResults sendTypingMessage(TypingAction action, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List messageIds) { + return null; + } + + @Override + public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List messageIds) { + return null; + } + + @Override + public SendMessageResults sendMessage(Message message, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendEditMessage(Message message, Set recipients, long targetSentTimestamp) { + return null; + } + + @Override + public SendMessageResults sendRemoteDeleteMessage(long targetSentTimestamp, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendMessageReaction(String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendAdminDelete(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendPinMessage(int duration, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendUnpinMessage(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendPaymentNotificationMessage(byte[] receipt, String note, RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public void sendEndSessionMessage(Set recipients) { + } + + @Override + public SendMessageResults sendMessageRequestResponse(MessageEnvelope.Sync.MessageRequestResponse.Type type, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendPollCreateMessage(String question, boolean multipleChoice, List options, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendPollVoteMessage(RecipientIdentifier.Single author, long timestamp, List optionIds, int version, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendPollTerminateMessage(long timestamp, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public void hideRecipient(RecipientIdentifier.Single recipient) { + } + + @Override + public void deleteRecipient(RecipientIdentifier.Single recipient) { + } + + @Override + public void deleteContact(RecipientIdentifier.Single recipient) { + } + + @Override + public void setContactName(RecipientIdentifier.Single recipient, String givenName, String familyName, String newGivenName, String newFamilyName, String nick) { + } + + @Override + public void setContactsBlocked(Collection recipients, boolean blocked) { + } + + @Override + public void setGroupsBlocked(Collection groupIds, boolean blocked) { + } + + @Override + public void setExpirationTimer(RecipientIdentifier.Single recipient, int messageExpirationTimer) { + } + + @Override + public StickerPackUrl uploadStickerPack(File path) { + return null; + } + + @Override + public void installStickerPack(StickerPackUrl url) { + } + + @Override + public List getStickerPacks() { + return List.of(); + } + + @Override + public void requestAllSyncData() { + } + + @Override + public boolean isContactBlocked(RecipientIdentifier.Single recipient) { + return false; + } + + @Override + public void sendContacts() { + } + + @Override + public List getRecipients(boolean onlyWithProfile, Optional blocked, Collection addresses, Optional name) { + return List.of(); + } + + @Override + public String getContactOrProfileName(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public Group getGroup(GroupId groupId) { + return null; + } + + @Override + public List getIdentities() { + return List.of(); + } + + @Override + public List getIdentities(RecipientIdentifier.Single recipient) { + return List.of(); + } + + @Override + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode) { + return false; + } + + @Override + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + return false; + } + + @Override + public void addAddressChangedListener(Runnable listener) { + } + + @Override + public void addClosedListener(Runnable listener) { + } + + @Override + public InputStream retrieveAttachment(String id) { + return null; + } + + @Override + public InputStream retrieveContactAvatar(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public InputStream retrieveProfileAvatar(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public InputStream retrieveGroupAvatar(GroupId groupId) { + return null; + } + + @Override + public InputStream retrieveSticker(StickerPackId stickerPackId, int stickerId) { + return null; + } + + @Override + public CallInfo startCall(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public CallInfo acceptCall(long callId) { + return null; + } + + @Override + public void hangupCall(long callId) { + } + + @Override + public SendMessageResult rejectCall(long callId) { + return null; + } + + @Override + public List listActiveCalls() { + return List.of(); + } + + @Override + public void sendCallOffer(RecipientIdentifier.Single recipient, CallOffer callOffer) { + } + + @Override + public void sendCallAnswer(RecipientIdentifier.Single recipient, long callId, byte[] answer) { + } + + @Override + public void sendIceUpdate(RecipientIdentifier.Single recipient, long callId, List iceCandidates) { + } + + @Override + public void sendHangup(RecipientIdentifier.Single recipient, long callId, MessageEnvelope.Call.Hangup.Type type) { + } + + @Override + public void sendBusy(RecipientIdentifier.Single recipient, long callId) { + } + + @Override + public List getTurnServerInfo() { + return List.of(); + } + + @Override + public void close() { + } + + @Override + public void addCallEventListener(CallEventListener listener) { + } + + @Override + public void removeCallEventListener(CallEventListener listener) { + } + } +} diff --git a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java index 8eaf4a72..b7a0ee61 100644 --- a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java +++ b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java @@ -333,8 +333,7 @@ class SubscribeCallEventsTest { } @Override - public SendMessageResults sendEndSessionMessage(Set r) { - return null; + public void sendEndSessionMessage(Set r) { } @Override