(recipientIds.size());
@@ -569,9 +592,9 @@ public class SendHelper {
}
}
- final var legacyTargets = new HashSet<>(recipientIds);
+ final var legacyTargets = new HashSet<>(targetRecipientIds);
legacyTargets.removeAll(senderKeyTargets);
- final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice();
+ final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice();
if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) {
if (!legacyTargets.isEmpty()) {
@@ -605,6 +628,7 @@ public class SendHelper {
isRecipientUpdate || !allResults.isEmpty());
allResults.addAll(results);
}
+ allResults.addAll(skippedResults);
final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime);
logger.debug("Sending took {}", duration.toString());
return allResults;
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 55795714..6eccb005 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
@@ -3,6 +3,7 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
+import org.signal.core.util.UptimeSleepTimer;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.SignalProtocolAddress;
@@ -40,7 +41,6 @@ import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
-import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection;
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 656b6646..10f7a2a9 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,10 +1,10 @@
package org.asamk.signal.manager.internal;
import org.jetbrains.annotations.NotNull;
+import org.signal.core.util.SleepTimer;
import org.signal.network.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
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 c80b3454..90ea1baa 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
@@ -463,6 +463,40 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
}
}
+ /**
+ * Returns the subset of the given recipients that are currently known to be
+ * unregistered (i.e. have an unregistered timestamp set).
+ *
+ * These can be skipped when sending group messages; otherwise every send
+ * re-attempts them via the slow legacy 1:1 fan-out. The unregistered flag is
+ * maintained independently by profile/CDS discovery and cleared again once a
+ * recipient registers, so they are re-included automatically.
+ */
+ public Set getUnregisteredRecipientIds(final Set recipientIds) {
+ if (recipientIds.isEmpty()) {
+ return Set.of();
+ }
+ final var recipientIdsCommaSeparated = recipientIds.stream()
+ .map(recipientId -> String.valueOf(recipientId.id()))
+ .collect(Collectors.joining(","));
+ final var sql = (
+ """
+ SELECT r._id
+ FROM %s r
+ WHERE r.unregistered_timestamp IS NOT NULL AND r._id IN (%s)
+ """
+ ).formatted(TABLE_RECIPIENT, recipientIdsCommaSeparated);
+ try (final var connection = database.getConnection()) {
+ try (final var statement = connection.prepareStatement(sql)) {
+ try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) {
+ return result.collect(Collectors.toSet());
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed read from recipient store", e);
+ }
+ }
+
public Set getAllNumbers() {
final var sql = (
"""
diff --git a/libsignal-version b/libsignal-version
index 6bc8ee7b..0a4eab77 100644
--- a/libsignal-version
+++ b/libsignal-version
@@ -1 +1 @@
-0.94.1
+0.94.4
diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java
index da2e5422..c6d9b40f 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.12.1");
+ .orElse("Signal-Android/8.15.0");
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/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
index dcaf06f3..d179aed0 100644
--- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
@@ -310,6 +310,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (callMessage.hangup().isPresent()) {
var hangupMessage = callMessage.hangup().get();
writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id()));
+ writer.println("- type: {}", hangupMessage.type());
+ writer.println("- device id: {}", hangupMessage.deviceId());
}
if (!callMessage.iceUpdate().isEmpty()) {
writer.println("Ice update messages:");
diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java
index 551c5b04..a6fc9f9e 100644
--- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java
+++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java
@@ -73,7 +73,13 @@ 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);
+ // If we're listening on any local address (0.0.0.0 or ::), skip Host header validation
+ final var addr = address == null ? null : address.getAddress();
+ if (addr != null && addr.isAnyLocalAddress()) {
+ logger.warn("HTTP server has no authentication; Host header validation DISABLED because listening on {}", address);
+ } else {
+ logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts);
+ }
}
@Override
@@ -306,6 +312,18 @@ public class HttpServerHandler implements AutoCloseable {
}
private boolean isHostAllowed(final HttpExchange httpExchange) {
+ // If the server is bound to any local address (0.0.0.0 or ::), skip host header validation
+ if (address != null) {
+ final var addr = address.getAddress();
+ if (addr != null && addr.isAnyLocalAddress()) {
+ return true;
+ }
+ final var hostStr = address.getHostString();
+ if ("0.0.0.0".equals(hostStr) || "::".equals(hostStr)) {
+ return true;
+ }
+ }
+
final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host");
if (hostHeader == null || hostHeader.isEmpty()) {
return false;
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 dff20afe..e69e2df6 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
@@ -776,6 +776,20 @@
"type": "java.lang.Class",
"jniAccessible": true,
"methods": [
+ {
+ "name": "forName",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ },
+ {
+ "name": "forName",
+ "parameterTypes": [
+ "java.lang.String",
+ "boolean",
+ "java.lang.ClassLoader"
+ ]
+ },
{
"name": "getCanonicalName",
"parameterTypes": []
@@ -784,6 +798,10 @@
"name": "getClassLoader",
"parameterTypes": []
},
+ {
+ "name": "getName",
+ "parameterTypes": []
+ },
{
"name": "getPermittedSubclasses",
"parameterTypes": []
@@ -819,7 +837,27 @@
]
},
{
- "type": "java.lang.ClassNotFoundException"
+ "type": "java.lang.ClassNotFoundException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": []
+ },
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ },
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String",
+ "java.lang.Throwable"
+ ]
+ }
+ ]
},
{
"type": "java.lang.ClassValue"
@@ -959,6 +997,18 @@
"name": "currentThread",
"parameterTypes": []
},
+ {
+ "name": "getContextClassLoader",
+ "parameterTypes": []
+ },
+ {
+ "name": "getId",
+ "parameterTypes": []
+ },
+ {
+ "name": "getName",
+ "parameterTypes": []
+ },
{
"name": "getStackTrace",
"parameterTypes": []
@@ -966,6 +1016,18 @@
{
"name": "isVirtual",
"parameterTypes": []
+ },
+ {
+ "name": "setContextClassLoader",
+ "parameterTypes": [
+ "java.lang.ClassLoader"
+ ]
+ },
+ {
+ "name": "setName",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
}
]
},
@@ -979,10 +1041,18 @@
"java.lang.Throwable"
]
},
+ {
+ "name": "getCause",
+ "parameterTypes": []
+ },
{
"name": "getMessage",
"parameterTypes": []
},
+ {
+ "name": "getStackTrace",
+ "parameterTypes": []
+ },
{
"name": "getSuppressed",
"parameterTypes": []
@@ -1586,7 +1656,42 @@
},
{
"type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter",
- "allPublicFields": true
+ "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"
+ }
+ ]
},
{
"type": "kotlinx.coroutines.AwaitAll"