1 package org
.asamk
.signal
.manager
.storage
.accounts
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
5 import org
.asamk
.signal
.manager
.api
.Pair
;
6 import org
.asamk
.signal
.manager
.api
.ServiceEnvironment
;
7 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
8 import org
.asamk
.signal
.manager
.storage
.Utils
;
9 import org
.asamk
.signal
.manager
.util
.IOUtils
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
13 import org
.whispersystems
.signalservice
.api
.util
.PhoneNumberFormatter
;
15 import java
.io
.ByteArrayInputStream
;
16 import java
.io
.ByteArrayOutputStream
;
18 import java
.io
.IOException
;
19 import java
.io
.RandomAccessFile
;
20 import java
.nio
.channels
.Channels
;
21 import java
.nio
.channels
.FileChannel
;
22 import java
.nio
.channels
.FileLock
;
23 import java
.util
.Arrays
;
24 import java
.util
.List
;
25 import java
.util
.Objects
;
26 import java
.util
.Random
;
28 import java
.util
.function
.Function
;
29 import java
.util
.stream
.Collectors
;
30 import java
.util
.stream
.Stream
;
32 public class AccountsStore
{
34 private static final int MINIMUM_STORAGE_VERSION
= 1;
35 private static final int CURRENT_STORAGE_VERSION
= 2;
36 private static final Logger logger
= LoggerFactory
.getLogger(AccountsStore
.class);
37 private final ObjectMapper objectMapper
= Utils
.createStorageObjectMapper();
39 private final File dataPath
;
40 private final String serviceEnvironment
;
41 private final AccountLoader accountLoader
;
44 final File dataPath
, final ServiceEnvironment serviceEnvironment
, final AccountLoader accountLoader
45 ) throws IOException
{
46 this.dataPath
= dataPath
;
47 this.serviceEnvironment
= getServiceEnvironmentString(serviceEnvironment
);
48 this.accountLoader
= accountLoader
;
49 if (!getAccountsFile().exists()) {
50 createInitialAccounts();
54 public synchronized Set
<String
> getAllNumbers() throws IOException
{
55 return readAccounts().stream()
56 .map(AccountsStorage
.Account
::number
)
57 .filter(Objects
::nonNull
)
58 .collect(Collectors
.toSet());
61 public synchronized Set
<AccountsStorage
.Account
> getAllAccounts() throws IOException
{
62 return readAccounts().stream()
63 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
64 .filter(a
-> a
.number() != null)
65 .collect(Collectors
.toSet());
68 public synchronized String
getPathByNumber(String number
) throws IOException
{
69 return readAccounts().stream()
70 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
71 .filter(a
-> number
.equals(a
.number()))
72 .map(AccountsStorage
.Account
::path
)
77 public synchronized String
getPathByAci(ACI aci
) throws IOException
{
78 return readAccounts().stream()
79 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
80 .filter(a
-> aci
.toString().equals(a
.uuid()))
81 .map(AccountsStorage
.Account
::path
)
86 public synchronized void updateAccount(String path
, String number
, ACI aci
) {
87 updateAccounts(accounts
-> accounts
.stream().map(a
-> {
88 if (a
.environment() != null && !serviceEnvironment
.equals(a
.environment())) {
92 if (path
.equals(a
.path())) {
93 return new AccountsStorage
.Account(a
.path(),
96 aci
== null ?
null : aci
.toString());
99 if (number
!= null && number
.equals(a
.number())) {
100 return new AccountsStorage
.Account(a
.path(), a
.environment(), null, a
.uuid());
102 if (aci
!= null && aci
.toString().equals(a
.toString())) {
103 return new AccountsStorage
.Account(a
.path(), a
.environment(), a
.number(), null);
110 public synchronized String
addAccount(String number
, ACI aci
) {
111 final var accountPath
= generateNewAccountPath();
112 final var account
= new AccountsStorage
.Account(accountPath
,
115 aci
== null ?
null : aci
.toString());
116 updateAccounts(accounts
-> {
117 final var existingAccounts
= accounts
.stream().map(a
-> {
118 if (a
.environment() != null && !serviceEnvironment
.equals(a
.environment())) {
122 if (number
!= null && number
.equals(a
.number())) {
123 return new AccountsStorage
.Account(a
.path(), a
.environment(), null, a
.uuid());
125 if (aci
!= null && aci
.toString().equals(a
.uuid())) {
126 return new AccountsStorage
.Account(a
.path(), a
.environment(), a
.number(), null);
131 return Stream
.concat(existingAccounts
, Stream
.of(account
)).toList();
136 public void removeAccount(final String accountPath
) {
137 updateAccounts(accounts
-> accounts
.stream().filter(a
-> !(
138 (a
.environment() == null || serviceEnvironment
.equals(a
.environment())) && a
.path().equals(accountPath
)
142 private String
generateNewAccountPath() {
143 return new Random().ints(100000, 1000000)
144 .mapToObj(String
::valueOf
)
145 .filter(n
-> !new File(dataPath
, n
).exists() && !new File(dataPath
, n
+ ".d").exists())
150 private File
getAccountsFile() {
151 return new File(dataPath
, "accounts.json");
154 private void createInitialAccounts() throws IOException
{
155 final var legacyAccountPaths
= getLegacyAccountPaths();
156 final var accountsStorage
= new AccountsStorage(legacyAccountPaths
.stream()
157 .map(number
-> new AccountsStorage
.Account(number
, null, number
, null))
158 .toList(), CURRENT_STORAGE_VERSION
);
160 IOUtils
.createPrivateDirectories(dataPath
);
161 var fileName
= getAccountsFile();
162 if (!fileName
.exists()) {
163 IOUtils
.createPrivateFile(fileName
);
166 final var pair
= openFileChannel(getAccountsFile());
167 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
168 saveAccountsLocked(fileChannel
, accountsStorage
);
172 private Set
<String
> getLegacyAccountPaths() {
173 final var files
= dataPath
.listFiles();
179 return Arrays
.stream(files
)
180 .filter(File
::isFile
)
182 .filter(file
-> PhoneNumberFormatter
.isValidNumber(file
, null))
183 .collect(Collectors
.toSet());
186 private List
<AccountsStorage
.Account
> readAccounts() throws IOException
{
187 final var pair
= openFileChannel(getAccountsFile());
188 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
189 final var storage
= readAccountsLocked(fileChannel
);
191 var accountsVersion
= storage
.version() == null ?
1 : storage
.version();
192 if (accountsVersion
> CURRENT_STORAGE_VERSION
) {
193 throw new IOException("Accounts file was created by a more recent version: " + accountsVersion
);
194 } else if (accountsVersion
< MINIMUM_STORAGE_VERSION
) {
195 throw new IOException("Accounts file was created by a no longer supported older version: "
197 } else if (accountsVersion
< CURRENT_STORAGE_VERSION
) {
198 return upgradeAccountsFile(fileChannel
, storage
, accountsVersion
).accounts();
200 return storage
.accounts();
204 private AccountsStorage
upgradeAccountsFile(
205 final FileChannel fileChannel
, final AccountsStorage storage
, final int accountsVersion
208 List
<AccountsStorage
.Account
> newAccounts
= storage
.accounts();
209 if (accountsVersion
< 2) {
210 // add environment field
211 newAccounts
= newAccounts
.stream().map(a
-> {
212 if (a
.environment() != null) {
215 try (final var account
= accountLoader
.loadAccountOrNull(a
.path())) {
216 if (account
== null || account
.getServiceEnvironment() == null) {
219 return new AccountsStorage
.Account(a
.path(),
220 getServiceEnvironmentString(account
.getServiceEnvironment()),
226 final var newStorage
= new AccountsStorage(newAccounts
, CURRENT_STORAGE_VERSION
);
227 saveAccountsLocked(fileChannel
, newStorage
);
229 } catch (Exception e
) {
230 logger
.warn("Failed to upgrade accounts file", e
);
235 private void updateAccounts(Function
<List
<AccountsStorage
.Account
>, List
<AccountsStorage
.Account
>> updater
) {
237 final var pair
= openFileChannel(getAccountsFile());
238 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
239 final var accountsStorage
= readAccountsLocked(fileChannel
);
240 final var newAccountsStorage
= updater
.apply(accountsStorage
.accounts());
241 saveAccountsLocked(fileChannel
, new AccountsStorage(newAccountsStorage
, CURRENT_STORAGE_VERSION
));
243 } catch (IOException e
) {
244 logger
.error("Failed to update accounts list", e
);
248 private AccountsStorage
readAccountsLocked(FileChannel fileChannel
) throws IOException
{
249 fileChannel
.position(0);
250 final var inputStream
= Channels
.newInputStream(fileChannel
);
251 return objectMapper
.readValue(inputStream
, AccountsStorage
.class);
254 private void saveAccountsLocked(FileChannel fileChannel
, AccountsStorage accountsStorage
) throws IOException
{
255 try (var output
= new ByteArrayOutputStream()) {
256 // Write to memory first to prevent corrupting the file in case of serialization errors
257 objectMapper
.writeValue(output
, accountsStorage
);
258 var input
= new ByteArrayInputStream(output
.toByteArray());
259 fileChannel
.position(0);
260 input
.transferTo(Channels
.newOutputStream(fileChannel
));
261 fileChannel
.truncate(fileChannel
.position());
262 fileChannel
.force(false);
266 private static Pair
<FileChannel
, FileLock
> openFileChannel(File fileName
) throws IOException
{
267 var fileChannel
= new RandomAccessFile(fileName
, "rw").getChannel();
268 var lock
= fileChannel
.tryLock();
270 logger
.info("Config file is in use by another instance, waiting…");
271 lock
= fileChannel
.lock();
272 logger
.info("Config file lock acquired.");
274 return new Pair
<>(fileChannel
, lock
);
277 private String
getServiceEnvironmentString(final ServiceEnvironment serviceEnvironment
) {
278 return switch (serviceEnvironment
) {
280 case STAGING
-> "STAGING";
284 public interface AccountLoader
{
286 SignalAccount
loadAccountOrNull(String accountPath
);