Improve handling of inbound UD messages.
This commit is contained in:
parent
bfdedd57d1
commit
b5dcf8e8f1
13 changed files with 60 additions and 28 deletions
|
@ -65,16 +65,17 @@ public class ProfileKeySendJob extends BaseJob {
|
||||||
|
|
||||||
if (queueLimits) {
|
if (queueLimits) {
|
||||||
return new ProfileKeySendJob(new Parameters.Builder()
|
return new ProfileKeySendJob(new Parameters.Builder()
|
||||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
.setQueue("ProfileKeySendJob_" + conversationRecipient.getId().toQueueKey())
|
||||||
|
.setMaxInstancesForQueue(1)
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.addConstraint(DecryptionsDrainedConstraint.KEY)
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
.build(), threadId, recipients);
|
.build(), threadId, recipients);
|
||||||
} else {
|
} else {
|
||||||
return new ProfileKeySendJob(new Parameters.Builder()
|
return new ProfileKeySendJob(new Parameters.Builder()
|
||||||
.setQueue("ProfileKeySendJob_" + conversationRecipient.getId().toQueueKey())
|
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.addConstraint(DecryptionsDrainedConstraint.KEY)
|
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
.build(), threadId, recipients);
|
.build(), threadId, recipients);
|
||||||
|
|
|
@ -285,7 +285,8 @@ public final class MessageContentProcessor {
|
||||||
.enqueue();
|
.enqueue();
|
||||||
} else if (!threadRecipient.isGroup()) {
|
} else if (!threadRecipient.isGroup()) {
|
||||||
Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key.");
|
Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key.");
|
||||||
ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false))
|
ApplicationDependencies.getJobManager()
|
||||||
|
.startChain(new RefreshAttributesJob(false))
|
||||||
.then(ProfileKeySendJob.create(context, SignalDatabase.threads().getOrCreateThreadIdFor(threadRecipient), true))
|
.then(ProfileKeySendJob.create(context, SignalDatabase.threads().getOrCreateThreadIdFor(threadRecipient), true))
|
||||||
.enqueue();
|
.enqueue();
|
||||||
}
|
}
|
||||||
|
@ -1722,13 +1723,12 @@ public final class MessageContentProcessor {
|
||||||
@NonNull byte[] messageProfileKeyBytes,
|
@NonNull byte[] messageProfileKeyBytes,
|
||||||
@NonNull Recipient senderRecipient)
|
@NonNull Recipient senderRecipient)
|
||||||
{
|
{
|
||||||
log(content.getTimestamp(), "Profile key.");
|
|
||||||
|
|
||||||
RecipientDatabase database = SignalDatabase.recipients();
|
RecipientDatabase database = SignalDatabase.recipients();
|
||||||
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
|
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
|
||||||
|
|
||||||
if (messageProfileKey != null) {
|
if (messageProfileKey != null) {
|
||||||
if (database.setProfileKey(senderRecipient.getId(), messageProfileKey)) {
|
if (database.setProfileKey(senderRecipient.getId(), messageProfileKey)) {
|
||||||
|
log(content.getTimestamp(), "Profile key on message from " + senderRecipient.getId() + " didn't match our local store. It has been updated.");
|
||||||
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(senderRecipient.getId()));
|
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(senderRecipient.getId()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1623,7 +1623,7 @@ public class SignalServiceMessageSender {
|
||||||
if (!unidentifiedAccess.isPresent()) {
|
if (!unidentifiedAccess.isPresent()) {
|
||||||
try {
|
try {
|
||||||
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.absent()).blockingGet()).getResultOrThrow();
|
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.absent()).blockingGet()).getResultOrThrow();
|
||||||
return SendMessageResult.success(recipient, messages.getDevices(), false, response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||||
} catch (WebSocketUnavailableException e) {
|
} catch (WebSocketUnavailableException e) {
|
||||||
Log.i(TAG, "[sendMessage][" + timestamp + "] Pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
Log.i(TAG, "[sendMessage][" + timestamp + "] Pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -1633,7 +1633,7 @@ public class SignalServiceMessageSender {
|
||||||
} else if (unidentifiedAccess.isPresent()) {
|
} else if (unidentifiedAccess.isPresent()) {
|
||||||
try {
|
try {
|
||||||
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess).blockingGet()).getResultOrThrow();
|
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess).blockingGet()).getResultOrThrow();
|
||||||
return SendMessageResult.success(recipient, messages.getDevices(), true, response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||||
} catch (WebSocketUnavailableException e) {
|
} catch (WebSocketUnavailableException e) {
|
||||||
Log.i(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
Log.i(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -1648,7 +1648,7 @@ public class SignalServiceMessageSender {
|
||||||
|
|
||||||
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess);
|
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess);
|
||||||
|
|
||||||
return SendMessageResult.success(recipient, messages.getDevices(), unidentifiedAccess.isPresent(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||||
|
|
||||||
} catch (InvalidKeyException ike) {
|
} catch (InvalidKeyException ike) {
|
||||||
Log.w(TAG, ike);
|
Log.w(TAG, ike);
|
||||||
|
|
|
@ -36,6 +36,7 @@ import org.whispersystems.libsignal.SessionCipher;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||||
import org.whispersystems.libsignal.groups.GroupCipher;
|
import org.whispersystems.libsignal.groups.GroupCipher;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||||
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
|
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
|
||||||
import org.whispersystems.libsignal.protocol.SignalMessage;
|
import org.whispersystems.libsignal.protocol.SignalMessage;
|
||||||
|
@ -202,9 +203,15 @@ public class SignalServiceCipher {
|
||||||
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp());
|
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp());
|
||||||
SignalServiceAddress resultAddress = new SignalServiceAddress(ACI.parseOrThrow(result.getSenderUuid()), result.getSenderE164());
|
SignalServiceAddress resultAddress = new SignalServiceAddress(ACI.parseOrThrow(result.getSenderUuid()), result.getSenderE164());
|
||||||
Optional<byte[]> groupId = result.getGroupId();
|
Optional<byte[]> groupId = result.getGroupId();
|
||||||
|
boolean needsReceipt = true;
|
||||||
|
|
||||||
|
if (envelope.hasSourceUuid()) {
|
||||||
|
Log.w(TAG, "[" + envelope.getTimestamp() + "] Received a UD-encrypted message sent over an identified channel. Marking as needsReceipt=false");
|
||||||
|
needsReceipt = false;
|
||||||
|
}
|
||||||
|
|
||||||
paddedMessage = result.getPaddedMessage();
|
paddedMessage = result.getPaddedMessage();
|
||||||
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid(), groupId);
|
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), needsReceipt, envelope.getServerGuid(), groupId);
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.services;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
|
@ -52,9 +53,11 @@ public class MessagingService {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ResponseMapper<SendMessageResponse> responseMapper = DefaultResponseMapper.extend(SendMessageResponse.class)
|
ResponseMapper<SendMessageResponse> responseMapper = DefaultResponseMapper.extend(SendMessageResponse.class)
|
||||||
.withResponseMapper((status, body, getHeader) -> {
|
.withResponseMapper((status, body, getHeader, unidentified) -> {
|
||||||
SendMessageResponse sendMessageResponse = Util.isEmpty(body) ? new SendMessageResponse(false)
|
SendMessageResponse sendMessageResponse = Util.isEmpty(body) ? new SendMessageResponse(false, unidentified)
|
||||||
: JsonUtil.fromJsonResponse(body, SendMessageResponse.class);
|
: JsonUtil.fromJsonResponse(body, SendMessageResponse.class);
|
||||||
|
sendMessageResponse.setSentUnidentfied(unidentified);
|
||||||
|
|
||||||
return ServiceResponse.forResult(sendMessageResponse, status, body);
|
return ServiceResponse.forResult(sendMessageResponse, status, body);
|
||||||
})
|
})
|
||||||
.withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found")))
|
.withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found")))
|
||||||
|
|
|
@ -125,7 +125,7 @@ public final class ProfileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServiceResponse<ProfileAndCredential> map(int status, String body, Function<String, String> getHeader)
|
public ServiceResponse<ProfileAndCredential> map(int status, String body, Function<String, String> getHeader, boolean unidentified)
|
||||||
throws MalformedResponseException
|
throws MalformedResponseException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import java.util.concurrent.ExecutionException;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates a parsed APi response regardless of where it came from (WebSocket or REST). Not only
|
* Encapsulates a parsed API response regardless of where it came from (WebSocket or REST). Not only
|
||||||
* includes the success result but also any application errors encountered (404s, parsing, etc.) or
|
* includes the success result but also any application errors encountered (404s, parsing, etc.) or
|
||||||
* execution errors encountered (IOException, etc.).
|
* execution errors encountered (IOException, etc.).
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -506,7 +506,7 @@ public class PushServiceSocket {
|
||||||
try {
|
try {
|
||||||
String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess);
|
String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess);
|
||||||
|
|
||||||
if (responseText == null) return new SendMessageResponse(false);
|
if (responseText == null) return new SendMessageResponse(false, unidentifiedAccess.isPresent());
|
||||||
else return JsonUtil.fromJson(responseText, SendMessageResponse.class);
|
else return JsonUtil.fromJson(responseText, SendMessageResponse.class);
|
||||||
} catch (NotFoundException nfe) {
|
} catch (NotFoundException nfe) {
|
||||||
throw new UnregisteredUserException(bundle.getDestination(), nfe);
|
throw new UnregisteredUserException(bundle.getDestination(), nfe);
|
||||||
|
@ -517,7 +517,7 @@ public class PushServiceSocket {
|
||||||
ListenableFuture<String> response = submitServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess);
|
ListenableFuture<String> response = submitServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess);
|
||||||
|
|
||||||
return FutureTransformers.map(response, body -> {
|
return FutureTransformers.map(response, body -> {
|
||||||
return body == null ? new SendMessageResponse(false)
|
return body == null ? new SendMessageResponse(false, unidentifiedAccess.isPresent())
|
||||||
: JsonUtil.fromJson(body, SendMessageResponse.class);
|
: JsonUtil.fromJson(body, SendMessageResponse.class);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
package org.whispersystems.signalservice.internal.push;
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
public class SendMessageResponse {
|
public class SendMessageResponse {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
private boolean needsSync;
|
private boolean needsSync;
|
||||||
|
|
||||||
|
private boolean sentUnidentfied;
|
||||||
|
|
||||||
public SendMessageResponse() {}
|
public SendMessageResponse() {}
|
||||||
|
|
||||||
public SendMessageResponse(boolean needsSync) {
|
public SendMessageResponse(boolean needsSync, boolean sentUnidentified) {
|
||||||
this.needsSync = needsSync;
|
this.needsSync = needsSync;
|
||||||
|
this.sentUnidentfied = sentUnidentified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getNeedsSync() {
|
public boolean getNeedsSync() {
|
||||||
return needsSync;
|
return needsSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean sentUnidentified() {
|
||||||
|
return sentUnidentfied;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSentUnidentfied(boolean value) {
|
||||||
|
this.sentUnidentfied = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,12 +41,12 @@ public class DefaultResponseMapper<Response> implements ResponseMapper<Response>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServiceResponse<Response> map(int status, String body, Function<String, String> getHeader) {
|
public ServiceResponse<Response> map(int status, String body, Function<String, String> getHeader, boolean unidentified) {
|
||||||
Throwable applicationError = errorMapper.parseError(status, body, getHeader);
|
Throwable applicationError = errorMapper.parseError(status, body, getHeader);
|
||||||
if (applicationError == null) {
|
if (applicationError == null) {
|
||||||
try {
|
try {
|
||||||
if (customResponseMapper != null) {
|
if (customResponseMapper != null) {
|
||||||
return Objects.requireNonNull(customResponseMapper.map(status, body, getHeader));
|
return Objects.requireNonNull(customResponseMapper.map(status, body, getHeader, unidentified));
|
||||||
}
|
}
|
||||||
return ServiceResponse.forResult(JsonUtil.fromJsonResponse(body, clazz), status, body);
|
return ServiceResponse.forResult(JsonUtil.fromJsonResponse(body, clazz), status, body);
|
||||||
} catch (MalformedResponseException e) {
|
} catch (MalformedResponseException e) {
|
||||||
|
@ -81,6 +81,6 @@ public class DefaultResponseMapper<Response> implements ResponseMapper<Response>
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface CustomResponseMapper<T> {
|
public interface CustomResponseMapper<T> {
|
||||||
ServiceResponse<T> map(int status, String body, Function<String, String> getHeader) throws MalformedResponseException;
|
ServiceResponse<T> map(int status, String body, Function<String, String> getHeader, boolean unidentified) throws MalformedResponseException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||||
* @param <T> - The final type the API response will map into.
|
* @param <T> - The final type the API response will map into.
|
||||||
*/
|
*/
|
||||||
public interface ResponseMapper<T> {
|
public interface ResponseMapper<T> {
|
||||||
ServiceResponse<T> map(int status, String body, Function<String, String> getHeader);
|
ServiceResponse<T> map(int status, String body, Function<String, String> getHeader, boolean unidentified);
|
||||||
|
|
||||||
default ServiceResponse<T> map(WebsocketResponse response) {
|
default ServiceResponse<T> map(WebsocketResponse response) {
|
||||||
return map(response.getStatus(), response.getBody(), response::getHeader);
|
return map(response.getStatus(), response.getBody(), response::getHeader, response.isUnidentified());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,7 +266,8 @@ public class WebSocketConnection extends WebSocketListener {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onSuccess(new WebsocketResponse(message.getResponse().getStatus(),
|
listener.onSuccess(new WebsocketResponse(message.getResponse().getStatus(),
|
||||||
new String(message.getResponse().getBody().toByteArray()),
|
new String(message.getResponse().getBody().toByteArray()),
|
||||||
message.getResponse().getHeadersList()));
|
message.getResponse().getHeadersList(),
|
||||||
|
!credentialsProvider.isPresent()));
|
||||||
if (message.getResponse().getStatus() >= 400) {
|
if (message.getResponse().getStatus() >= 400) {
|
||||||
healthMonitor.onMessageError(message.getResponse().getStatus(), credentialsProvider.isPresent());
|
healthMonitor.onMessageError(message.getResponse().getStatus(), credentialsProvider.isPresent());
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,13 @@ public class WebsocketResponse {
|
||||||
private final int status;
|
private final int status;
|
||||||
private final String body;
|
private final String body;
|
||||||
private final Map<String, String> headers;
|
private final Map<String, String> headers;
|
||||||
|
private final boolean unidentified;
|
||||||
|
|
||||||
WebsocketResponse(int status, String body, List<String> headers) {
|
WebsocketResponse(int status, String body, List<String> headers, boolean unidentified) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.body = body;
|
this.body = body;
|
||||||
this.headers = parseHeaders(headers);
|
this.headers = parseHeaders(headers);
|
||||||
|
this.unidentified = unidentified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getStatus() {
|
public int getStatus() {
|
||||||
|
@ -29,6 +31,10 @@ public class WebsocketResponse {
|
||||||
return headers.get(Preconditions.checkNotNull(key.toLowerCase()));
|
return headers.get(Preconditions.checkNotNull(key.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUnidentified() {
|
||||||
|
return unidentified;
|
||||||
|
}
|
||||||
|
|
||||||
private static Map<String, String> parseHeaders(List<String> rawHeaders) {
|
private static Map<String, String> parseHeaders(List<String> rawHeaders) {
|
||||||
Map<String, String> headers = new HashMap<>(rawHeaders.size());
|
Map<String, String> headers = new HashMap<>(rawHeaders.size());
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue