Compare commits

...

7 Commits

Author SHA1 Message Date
Stefan Meinecke
62ed3069d9
Merge 67105e14c19d9abf09a3ab133bd43da13662a7d6 into 056878fad7a74dc38a2ac74a056121e958d631a0 2026-04-23 10:23:53 +00:00
Stefan Meinecke
67105e14c1
Merge branch 'master' into fix/unauthenticated-socket-keepalive 2026-04-23 10:23:51 +00:00
AsamK
056878fad7 Bump version to 0.14.3 2026-04-22 23:16:58 +02:00
Stefan Meinecke
f27eb524de Merge branch 'master' of https://github.com/AsamK/signal-cli into fix/unauthenticated-socket-keepalive 2026-04-17 11:49:13 +02:00
Stefan Meinecke
0006fd0dc0 Tie unauthenticated WebSocket keep-alive to active client connections
Keep the unidentified socket alive while a JSON-RPC connection is open (including stdio mode) and while a D-Bus object is exported, instead of for the lifetime of the receive loop. The receive loop does not use the unauthenticated socket, so keeping it alive there was semantically wrong.

This also covers --receive-mode=manual, where no receive loop runs butclients still send messages.
2026-04-17 11:49:05 +02:00
Stefan Meinecke
72332750a8 Merge branch 'master' of https://github.com/AsamK/signal-cli into fix/unauthenticated-socket-keepalive 2026-04-15 11:42:43 +02:00
Stefan Meinecke
8310b16895 Keep unauthenticated WebSocket alive during daemon receive loop
The unauthenticated (sealed sender) socket had no keep-alive token
registered, causing SignalWebSocket's DelayedDisconnectThread to tear
down the connection ~10s after each send. Every subsequent group message
then had to re-establish a fresh TLS connection (~6s delay).

The authenticated socket avoids this by registering a "receive" keep-alive
token for the lifetime of the receive loop. Apply the same pattern to the
unauthenticated socket: register the token alongside the authenticated one
and remove it in the same finally block.

This keeps the unidentified connection alive in daemon mode, matching the
behaviour of Signal mobile clients.
2026-04-14 19:59:42 +02:00
10 changed files with 78 additions and 5 deletions

View File

@ -1,10 +1,22 @@
# Changelog
## [Unreleased]
## [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

View File

@ -10,7 +10,7 @@ plugins {
allprojects {
group = "org.asamk"
version = "0.14.3-SNAPSHOT"
version = "0.14.3"
}
java {

View File

@ -45,6 +45,9 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="0.14.3" date="2026-04-22">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
</release>
<release version="0.14.2" date="2026-04-04">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
</release>

View File

@ -407,6 +407,10 @@ public interface Manager extends Closeable {
void addClosedListener(Runnable listener);
void addUnidentifiedKeepAlive(String token);
void removeUnidentifiedKeepAlive(String token);
InputStream retrieveAttachment(final String id) throws IOException;
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;

View File

@ -1712,6 +1712,16 @@ public class ManagerImpl implements Manager {
}
}
@Override
public void addUnidentifiedKeepAlive(final String token) {
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken(token);
}
@Override
public void removeUnidentifiedKeepAlive(final String token) {
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken(token);
}
@Override
public void addCallEventListener(final CallEventListener listener) {
context.getCallManager().addCallEventListener(listener);

View File

@ -1 +1 @@
0.90.0
0.92.1

View File

@ -916,6 +916,14 @@ public class DbusManagerImpl implements Manager {
}
}
@Override
public void addUnidentifiedKeepAlive(final String token) {
}
@Override
public void removeUnidentifiedKeepAlive(final String token) {
}
@Override
public void addCallEventListener(final CallEventListener listener) {
// Not supported over DBus

View File

@ -100,6 +100,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
public void initObjects() {
exportObjects();
m.addUnidentifiedKeepAlive("dbus");
if (!noReceiveOnStart) {
subscribeReceive();
}
@ -116,6 +117,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override
public void close() {
m.removeUnidentifiedKeepAlive("dbus");
if (dbusMessageHandler != null) {
m.removeReceiveHandler(dbusMessageHandler);
dbusMessageHandler = null;

View File

@ -25,10 +25,12 @@ 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.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@ -43,6 +45,9 @@ public class SignalJsonRpcDispatcherHandler {
private final Map<Integer, List<Pair<Manager, Manager.ReceiveMessageHandler>>> receiveHandlers = new HashMap<>();
private final Map<Integer, List<Pair<Manager, Manager.CallEventListener>>> callEventHandlers = new HashMap<>();
private final String connectionKeepAliveToken = "jsonrpc-" + UUID.randomUUID();
private final List<Manager> keepAliveManagers = new ArrayList<>();
private boolean connectionActive = true;
private SignalJsonRpcCommandHandler commandHandler;
public SignalJsonRpcDispatcherHandler(
@ -69,6 +74,10 @@ public class SignalJsonRpcDispatcherHandler {
c.addOnManagerAddedHandler(m -> callEventHandlers.forEach((subscriptionId, handlers) -> handlers.add(
createCallEventHandler(m, subscriptionId))));
c.getManagers().forEach(this::registerKeepAlive);
c.addOnManagerAddedHandler(this::registerKeepAlive);
c.addOnManagerRemovedHandler(this::unregisterKeepAlive);
handleConnection();
}
@ -82,6 +91,8 @@ public class SignalJsonRpcDispatcherHandler {
final var currentThread = Thread.currentThread();
m.addClosedListener(currentThread::interrupt);
registerKeepAlive(m);
handleConnection();
}
@ -200,14 +211,29 @@ public class SignalJsonRpcDispatcherHandler {
subscriptionId.ifPresent(this::unsubscribeReceive);
}
private void registerKeepAlive(final Manager m) {
if (!connectionActive) return;
m.addUnidentifiedKeepAlive(connectionKeepAliveToken);
keepAliveManagers.add(m);
}
private void unregisterKeepAlive(final Manager m) {
if (!connectionActive) return;
m.removeUnidentifiedKeepAlive(connectionKeepAliveToken);
keepAliveManagers.remove(m);
}
private void handleConnection() {
try {
jsonRpcReader.readMessages((method, params) -> commandHandler.handleRequest(objectMapper, method, params),
response -> logger.debug("Received unexpected response for id {}", response.getId()));
} finally {
connectionActive = false;
receiveHandlers.forEach((_subscriptionId, handlers) -> handlers.forEach(this::unsubscribeReceiveHandler));
receiveHandlers.clear();
unsubscribeAllCallEvents();
keepAliveManagers.forEach(m -> m.removeUnidentifiedKeepAlive(connectionKeepAliveToken));
keepAliveManagers.clear();
}
}

View File

@ -496,6 +496,14 @@ class SubscribeCallEventsTest {
public void addClosedListener(Runnable l) {
}
@Override
public void addUnidentifiedKeepAlive(String token) {
}
@Override
public void removeUnidentifiedKeepAlive(String token) {
}
@Override
public InputStream retrieveAttachment(String id) {
return null;
@ -741,8 +749,8 @@ class SubscribeCallEventsTest {
assertEquals(1, manager1.addCount.get(), "manager1 should have one listener");
assertEquals(1, manager2.addCount.get(), "manager2 should have one listener");
// Also registers an onManagerAdded handler for receive and one for call events
assertEquals(2, multi.addedHandlers.size(), "should register onManagerAdded handlers");
// Registers onManagerAdded handlers for receive, call events, and keep-alive
assertEquals(3, multi.addedHandlers.size(), "should register onManagerAdded handlers");
}
@Test