mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-05-12 12:20:52 +00:00
Use Jackson JSON serialization in CallManager
Replace all manual JSON string concatenation with Jackson ObjectNode construction and ObjectMapper serialization. Use BigInteger for call IDs to properly represent unsigned 64-bit values in JSON. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a481584c3a
commit
e6dea074c3
@ -10,6 +10,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -27,6 +28,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.SecureRandom;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -116,9 +118,11 @@ public class CallManager implements AutoCloseable {
|
||||
var turnServers = getTurnServers();
|
||||
|
||||
// Send createOutgoingCall + proceed via control channel
|
||||
var peerIdStr = recipientAddress.toString();
|
||||
sendControlMessage(state, "{\"type\":\"createOutgoingCall\",\"callId\":" + callIdJson(callId)
|
||||
+ ",\"peerId\":\"" + escapeJson(peerIdStr) + "\"}");
|
||||
var createMsg = mapper.createObjectNode();
|
||||
createMsg.put("type", "createOutgoingCall");
|
||||
createMsg.put("callId", callIdUnsigned(callId));
|
||||
createMsg.put("peerId", recipientAddress.toString());
|
||||
sendControlMessage(state, writeJson(createMsg));
|
||||
sendProceed(state, callId, turnServers);
|
||||
|
||||
// Schedule ring timeout
|
||||
@ -272,18 +276,16 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
|
||||
// Send receivedOffer to subprocess
|
||||
var opaqueB64 = java.util.Base64.getEncoder().encodeToString(opaque);
|
||||
var senderIdKeyB64 = java.util.Base64.getEncoder().encodeToString(remoteIdentityKey);
|
||||
var receiverIdKeyB64 = java.util.Base64.getEncoder().encodeToString(localIdentityKey);
|
||||
var peerIdStr = senderAddress.toString();
|
||||
sendControlMessage(state, "{\"type\":\"receivedOffer\",\"callId\":" + callIdJson(callId)
|
||||
+ ",\"peerId\":\"" + escapeJson(peerIdStr) + "\""
|
||||
+ ",\"senderDeviceId\":1"
|
||||
+ ",\"opaque\":\"" + opaqueB64 + "\""
|
||||
+ ",\"age\":0"
|
||||
+ ",\"senderIdentityKey\":\"" + senderIdKeyB64 + "\""
|
||||
+ ",\"receiverIdentityKey\":\"" + receiverIdKeyB64 + "\""
|
||||
+ "}");
|
||||
var offerMsg = mapper.createObjectNode();
|
||||
offerMsg.put("type", "receivedOffer");
|
||||
offerMsg.put("callId", callIdUnsigned(callId));
|
||||
offerMsg.put("peerId", senderAddress.toString());
|
||||
offerMsg.put("senderDeviceId", 1);
|
||||
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
offerMsg.put("age", 0);
|
||||
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||
offerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||
sendControlMessage(state, writeJson(offerMsg));
|
||||
|
||||
// Send proceed with TURN servers
|
||||
sendProceed(state, callId, turnServers);
|
||||
@ -309,15 +311,13 @@ public class CallManager implements AutoCloseable {
|
||||
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||
|
||||
// Forward raw opaque to subprocess
|
||||
var opaqueB64 = java.util.Base64.getEncoder().encodeToString(opaque);
|
||||
var senderIdKeyB64 = java.util.Base64.getEncoder().encodeToString(remoteIdentityKey);
|
||||
var receiverIdKeyB64 = java.util.Base64.getEncoder().encodeToString(localIdentityKey);
|
||||
sendControlMessage(state, "{\"type\":\"receivedAnswer\""
|
||||
+ ",\"opaque\":\"" + opaqueB64 + "\""
|
||||
+ ",\"senderDeviceId\":1"
|
||||
+ ",\"senderIdentityKey\":\"" + senderIdKeyB64 + "\""
|
||||
+ ",\"receiverIdentityKey\":\"" + receiverIdKeyB64 + "\""
|
||||
+ "}");
|
||||
var answerMsg = mapper.createObjectNode();
|
||||
answerMsg.put("type", "receivedAnswer");
|
||||
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
answerMsg.put("senderDeviceId", 1);
|
||||
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||
sendControlMessage(state, writeJson(answerMsg));
|
||||
|
||||
state.state = CallInfo.State.CONNECTING;
|
||||
fireCallEvent(state, null);
|
||||
@ -333,8 +333,11 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
|
||||
// Forward to subprocess as receivedIce
|
||||
var b64 = java.util.Base64.getEncoder().encodeToString(opaque);
|
||||
sendControlMessage(state, "{\"type\":\"receivedIce\",\"candidates\":[\"" + b64 + "\"]}");
|
||||
var iceMsg = mapper.createObjectNode();
|
||||
iceMsg.put("type", "receivedIce");
|
||||
var candidates = iceMsg.putArray("candidates");
|
||||
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
sendControlMessage(state, writeJson(iceMsg));
|
||||
logger.debug("Forwarded ICE candidate to tunnel for call {}", callId);
|
||||
}
|
||||
|
||||
@ -364,24 +367,21 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
|
||||
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
|
||||
var sb = new StringBuilder();
|
||||
sb.append("{\"type\":\"proceed\",\"callId\":").append(callIdJson(callId));
|
||||
sb.append(",\"hideIp\":false");
|
||||
sb.append(",\"iceServers\":[");
|
||||
for (int i = 0; i < turnServers.size(); i++) {
|
||||
if (i > 0) sb.append(",");
|
||||
var ts = turnServers.get(i);
|
||||
sb.append("{\"username\":\"").append(escapeJson(ts.username())).append("\"");
|
||||
sb.append(",\"password\":\"").append(escapeJson(ts.password())).append("\"");
|
||||
sb.append(",\"urls\":[");
|
||||
for (int j = 0; j < ts.urls().size(); j++) {
|
||||
if (j > 0) sb.append(",");
|
||||
sb.append("\"").append(escapeJson(ts.urls().get(j))).append("\"");
|
||||
var proceedMsg = mapper.createObjectNode();
|
||||
proceedMsg.put("type", "proceed");
|
||||
proceedMsg.put("callId", callIdUnsigned(callId));
|
||||
proceedMsg.put("hideIp", false);
|
||||
var iceServers = proceedMsg.putArray("iceServers");
|
||||
for (var ts : turnServers) {
|
||||
var server = iceServers.addObject();
|
||||
server.put("username", ts.username());
|
||||
server.put("password", ts.password());
|
||||
var urls = server.putArray("urls");
|
||||
for (var url : ts.urls()) {
|
||||
urls.add(url);
|
||||
}
|
||||
sb.append("]}");
|
||||
}
|
||||
sb.append("]}");
|
||||
sendControlMessage(state, sb.toString());
|
||||
sendControlMessage(state, writeJson(proceedMsg));
|
||||
}
|
||||
|
||||
private void spawnMediaTunnel(CallState state) {
|
||||
@ -476,15 +476,13 @@ public class CallManager implements AutoCloseable {
|
||||
new SecureRandom().nextBytes(tokenBytes);
|
||||
state.controlToken = java.util.Base64.getEncoder().encodeToString(tokenBytes);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
sb.append("\"call_id\":").append(callIdJson(state.callId));
|
||||
sb.append(",\"is_outgoing\":").append(state.isOutgoing);
|
||||
sb.append(",\"control_socket_path\":\"").append(escapeJson(state.controlSocketPath)).append("\"");
|
||||
sb.append(",\"control_token\":\"").append(state.controlToken).append("\"");
|
||||
sb.append(",\"local_device_id\":1");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
var config = mapper.createObjectNode();
|
||||
config.put("call_id", callIdUnsigned(state.callId));
|
||||
config.put("is_outgoing", state.isOutgoing);
|
||||
config.put("control_socket_path", state.controlSocketPath);
|
||||
config.put("control_token", state.controlToken);
|
||||
config.put("local_device_id", 1);
|
||||
return writeJson(config);
|
||||
}
|
||||
|
||||
private void connectToControlSocket(CallState state) {
|
||||
@ -503,7 +501,10 @@ public class CallManager implements AutoCloseable {
|
||||
new OutputStreamWriter(Channels.newOutputStream(channel), StandardCharsets.UTF_8), true);
|
||||
|
||||
// Send authentication token
|
||||
state.controlWriter.println("{\"type\":\"auth\",\"token\":\"" + state.controlToken + "\"}");
|
||||
var authMsg = mapper.createObjectNode();
|
||||
authMsg.put("type", "auth");
|
||||
authMsg.put("token", state.controlToken);
|
||||
state.controlWriter.println(writeJson(authMsg));
|
||||
logger.info("Connected to control socket for call {}", state.callId);
|
||||
|
||||
// Flush any pending control messages
|
||||
@ -636,7 +637,9 @@ public class CallManager implements AutoCloseable {
|
||||
if (state.acceptPending && state.controlWriter != null) {
|
||||
state.acceptPending = false;
|
||||
logger.debug("Sending deferred accept for call {}", state.callId);
|
||||
state.controlWriter.println("{\"type\":\"accept\"}");
|
||||
var acceptMsg = mapper.createObjectNode();
|
||||
acceptMsg.put("type", "accept");
|
||||
state.controlWriter.println(writeJson(acceptMsg));
|
||||
}
|
||||
}
|
||||
|
||||
@ -752,14 +755,17 @@ public class CallManager implements AutoCloseable {
|
||||
return serializedKey;
|
||||
}
|
||||
|
||||
/** Format call ID as unsigned for JSON (tunnel binary expects u64). */
|
||||
private static String callIdJson(long callId) {
|
||||
return Long.toUnsignedString(callId);
|
||||
/** Convert signed long call ID to unsigned BigInteger (tunnel binary expects u64). */
|
||||
private static BigInteger callIdUnsigned(long callId) {
|
||||
return new BigInteger(Long.toUnsignedString(callId));
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
|
||||
private static String writeJson(ObjectNode node) {
|
||||
try {
|
||||
return mapper.writeValueAsString(node);
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to serialize JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void endCall(final long callId, final String reason) {
|
||||
|
||||
@ -5,7 +5,6 @@ import org.asamk.signal.manager.api.RecipientAddress;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
@ -16,7 +15,6 @@ import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
@ -28,8 +26,7 @@ class CallManagerTest {
|
||||
// --- Reflection helpers for private static methods ---
|
||||
|
||||
private static final MethodHandle GET_RAW_IDENTITY_KEY_BYTES;
|
||||
private static final MethodHandle CALL_ID_JSON;
|
||||
private static final MethodHandle ESCAPE_JSON;
|
||||
private static final MethodHandle CALL_ID_UNSIGNED;
|
||||
private static final MethodHandle GENERATE_CALL_ID;
|
||||
|
||||
static {
|
||||
@ -39,11 +36,8 @@ class CallManagerTest {
|
||||
GET_RAW_IDENTITY_KEY_BYTES = lookup.findStatic(CallManager.class, "getRawIdentityKeyBytes",
|
||||
MethodType.methodType(byte[].class, byte[].class));
|
||||
|
||||
CALL_ID_JSON = lookup.findStatic(CallManager.class, "callIdJson",
|
||||
MethodType.methodType(String.class, long.class));
|
||||
|
||||
ESCAPE_JSON = lookup.findStatic(CallManager.class, "escapeJson",
|
||||
MethodType.methodType(String.class, String.class));
|
||||
CALL_ID_UNSIGNED = lookup.findStatic(CallManager.class, "callIdUnsigned",
|
||||
MethodType.methodType(BigInteger.class, long.class));
|
||||
|
||||
GENERATE_CALL_ID = lookup.findStatic(CallManager.class, "generateCallId",
|
||||
MethodType.methodType(long.class));
|
||||
@ -57,12 +51,8 @@ class CallManagerTest {
|
||||
return (byte[]) GET_RAW_IDENTITY_KEY_BYTES.invokeExact(serializedKey);
|
||||
}
|
||||
|
||||
private static String callIdJson(long callId) throws Throwable {
|
||||
return (String) CALL_ID_JSON.invokeExact(callId);
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) throws Throwable {
|
||||
return (String) ESCAPE_JSON.invokeExact(s);
|
||||
private static BigInteger callIdUnsigned(long callId) throws Throwable {
|
||||
return (BigInteger) CALL_ID_UNSIGNED.invokeExact(callId);
|
||||
}
|
||||
|
||||
private static long generateCallId() throws Throwable {
|
||||
@ -143,78 +133,34 @@ class CallManagerTest {
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// callIdJson tests
|
||||
// callIdUnsigned tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
void callIdJson_zero() throws Throwable {
|
||||
assertEquals("0", callIdJson(0L));
|
||||
void callIdUnsigned_zero() throws Throwable {
|
||||
assertEquals(BigInteger.ZERO, callIdUnsigned(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callIdJson_positiveLong() throws Throwable {
|
||||
assertEquals("8230211930154373276", callIdJson(8230211930154373276L));
|
||||
void callIdUnsigned_positiveLong() throws Throwable {
|
||||
assertEquals(new BigInteger("8230211930154373276"), callIdUnsigned(8230211930154373276L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callIdJson_negativeLongBecomesUnsigned() throws Throwable {
|
||||
void callIdUnsigned_negativeLongBecomesUnsigned() throws Throwable {
|
||||
// -1L as unsigned is 2^64 - 1 = 18446744073709551615
|
||||
assertEquals("18446744073709551615", callIdJson(-1L));
|
||||
assertEquals(new BigInteger("18446744073709551615"), callIdUnsigned(-1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callIdJson_longMinValueBecomesUnsigned() throws Throwable {
|
||||
void callIdUnsigned_longMinValueBecomesUnsigned() throws Throwable {
|
||||
// Long.MIN_VALUE as unsigned is 2^63 = 9223372036854775808
|
||||
assertEquals("9223372036854775808", callIdJson(Long.MIN_VALUE));
|
||||
assertEquals(new BigInteger("9223372036854775808"), callIdUnsigned(Long.MIN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callIdJson_longMaxValue() throws Throwable {
|
||||
assertEquals("9223372036854775807", callIdJson(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// escapeJson tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
void escapeJson_null() throws Throwable {
|
||||
assertEquals("", escapeJson(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_empty() throws Throwable {
|
||||
assertEquals("", escapeJson(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_noSpecialChars() throws Throwable {
|
||||
assertEquals("hello world", escapeJson("hello world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_backslash() throws Throwable {
|
||||
assertEquals("path\\\\to\\\\file", escapeJson("path\\to\\file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_doubleQuote() throws Throwable {
|
||||
assertEquals("say \\\"hello\\\"", escapeJson("say \"hello\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_newline() throws Throwable {
|
||||
assertEquals("line1\\nline2", escapeJson("line1\nline2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_carriageReturn() throws Throwable {
|
||||
assertEquals("line1\\rline2", escapeJson("line1\rline2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeJson_allSpecialChars() throws Throwable {
|
||||
assertEquals("a\\\\b\\\"c\\nd\\re", escapeJson("a\\b\"c\nd\re"));
|
||||
void callIdUnsigned_longMaxValue() throws Throwable {
|
||||
assertEquals(new BigInteger("9223372036854775807"), callIdUnsigned(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user