mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-06-09 16:50:19 +00:00
Merge branch 'master' into schemas-on-release
This commit is contained in:
commit
47c27e6de4
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,6 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
## [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
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.14.4-SNAPSHOT"
|
||||
version = "0.14.4"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -45,6 +45,9 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="0.14.4" date="2026-05-23">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
||||
</release>
|
||||
<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>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
[versions]
|
||||
slf4j = "2.0.18"
|
||||
junit = "6.0.3"
|
||||
micronaut-json-schema = "2.0.0"
|
||||
junit = "6.1.0"
|
||||
micronaut-json-schema = "2.0.1"
|
||||
micronaut-core = "5.0.0"
|
||||
signal-service = "2.15.3_unofficial_146"
|
||||
signal-service = "2.15.3_unofficial_147"
|
||||
|
||||
[libraries]
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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<GroupIdV1, StorageId> generateGroupV1StorageIds(List<GroupIdV1> groupIds) {
|
||||
return groupIds.stream()
|
||||
.collect(Collectors.toMap(recipientId -> recipientId,
|
||||
recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
||||
_ -> StorageId.forGroupV1(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private Map<GroupIdV2, StorageId> generateGroupV2StorageIds(List<GroupIdV2> groupIds) {
|
||||
return groupIds.stream()
|
||||
.collect(Collectors.toMap(recipientId -> recipientId,
|
||||
recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||
_ -> StorageId.forGroupV2(KeyUtils.createRawStorageId())));
|
||||
}
|
||||
|
||||
private void storeManifestLocally(
|
||||
@ -630,16 +635,17 @@ public class StorageHelper {
|
||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
||||
}
|
||||
|
||||
private List<StorageId> processKnownRecords(
|
||||
private Pair<List<StorageId>, List<StorageId>> processKnownRecords(
|
||||
final Connection connection,
|
||||
List<SignalStorageRecord> records
|
||||
) throws SQLException {
|
||||
final var unknownRecords = new ArrayList<StorageId>();
|
||||
final var processedRecords = new ArrayList<StorageId>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -101,6 +101,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;
|
||||
@ -117,7 +118,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;
|
||||
@ -190,6 +190,7 @@ public class ManagerImpl implements Manager {
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
account.getDeviceId(),
|
||||
executor,
|
||||
sessionLock);
|
||||
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
|
||||
|
||||
@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
0,
|
||||
null,
|
||||
new ReentrantSignalSessionLock());
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
|
||||
@ -5,13 +5,17 @@ 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;
|
||||
@ -21,12 +25,12 @@ 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.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.keys.PreKeyRepository;
|
||||
import org.whispersystems.signalservice.api.message.MessageApi;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
@ -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> 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;
|
||||
}
|
||||
@ -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<SecureValueRecovery> getSecureValueRecovery() {
|
||||
@ -368,7 +398,9 @@ public class SignalDependencies {
|
||||
|
||||
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
||||
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
|
||||
final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni();
|
||||
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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<String> 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<String> 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<String> filename, final Optional<String> contentType) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -878,7 +878,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
|
||||
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
||||
final Connection connection,
|
||||
final List<StorageId> storageIds
|
||||
final Collection<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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<E extends SignalRecord<?>> implemen
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class);
|
||||
private final Set<E> matchedRecords = new TreeSet<>(this);
|
||||
private final Set<StorageId> 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<E extends SignalRecord<?>> 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<E extends SignalRecord<?>> 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<E extends SignalRecord<?>> implemen
|
||||
}
|
||||
}
|
||||
|
||||
public Set<StorageId> getUpdatedStorageIds() {
|
||||
return Collections.unmodifiableSet(updatedStorageIds);
|
||||
}
|
||||
|
||||
private void debug(StorageId i, E record, String message) {
|
||||
logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message);
|
||||
}
|
||||
|
||||
@ -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<PosixFilePermission> 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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
0.92.1
|
||||
0.94.1
|
||||
|
||||
@ -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[/_<phonenumber>] org.asamk.Signal.<method> [string:<string argument>] [array:<type>:<array argument>]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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`))
|
||||
|
||||
@ -1172,10 +1172,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 <asamk@gmx.de>, who is assisted by other open source contributors.
|
||||
|
||||
@ -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());
|
||||
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());
|
||||
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());
|
||||
mut.addArgument("--dbus-system")
|
||||
.dest("global-dbus-system")
|
||||
.help("Make request via system dbus.")
|
||||
.action(Arguments.storeTrue());
|
||||
.action(Arguments.storeTrue())
|
||||
.setDefault(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());
|
||||
|
||||
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.<ServiceEnvironmentCli>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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.8.0");
|
||||
.orElse("Signal-Android/8.12.1");
|
||||
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
|
||||
? "signal-cli"
|
||||
: PROJECT_NAME + "/" + PROJECT_VERSION;
|
||||
|
||||
81
src/main/java/org/asamk/signal/ConfigLoader.java
Normal file
81
src/main/java/org/asamk/signal/ConfigLoader.java
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
36
src/main/java/org/asamk/signal/GlobalConfig.java
Normal file
36
src/main/java/org/asamk/signal/GlobalConfig.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 ? null : 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 ? null : 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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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;
|
||||
@ -273,4 +292,59 @@ public class HttpServerHandler implements AutoCloseable {
|
||||
|
||||
void call();
|
||||
}
|
||||
|
||||
private Set<String> buildAllowedHosts(final InetSocketAddress address) {
|
||||
final var s = new HashSet<String>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1317,6 +1317,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "java.util.concurrent.CopyOnWriteArrayList",
|
||||
"fields": [
|
||||
{
|
||||
"name": "lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "java.util.concurrent.ForkJoinTask",
|
||||
"fields": [
|
||||
@ -1979,6 +1987,28 @@
|
||||
{
|
||||
"type": "org.asamk.SignalControl.Error.InvalidNumber"
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.GlobalConfig",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"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,
|
||||
@ -1995,6 +2025,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,
|
||||
@ -6488,6 +6543,33 @@
|
||||
"type": "org.signal.libsignal.zkgroup.profiles.ProfileKey",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "org.signal.network.api.SenderCertificate",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "setCertificate",
|
||||
"parameterTypes": [
|
||||
"byte[]"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.signal.network.api.SenderCertificate$ByteArrayDeserializer",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.signal.network.api.SenderCertificate$ByteArraySerializer"
|
||||
},
|
||||
{
|
||||
"type": "org.signal.storageservice.storage.protos.groups.AccessControl",
|
||||
"fields": [
|
||||
|
||||
@ -39,7 +39,7 @@ import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -97,7 +97,7 @@ class SseInitialFlushTest {
|
||||
@Test
|
||||
void sseEndpointReturnsHeadersWithinTwoSeconds() {
|
||||
assertDoesNotThrow(() -> {
|
||||
var url = new URL("http://127.0.0.1:" + port + "/api/v1/events");
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user