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
;
45 final ServiceEnvironment serviceEnvironment
,
46 final AccountLoader accountLoader
47 ) throws IOException
{
48 this.dataPath
= dataPath
;
49 this.serviceEnvironment
= getServiceEnvironmentString(serviceEnvironment
);
50 this.accountLoader
= accountLoader
;
51 if (!getAccountsFile().exists()) {
52 createInitialAccounts();
56 public synchronized Set
<String
> getAllNumbers() throws IOException
{
57 return readAccounts().stream()
58 .map(AccountsStorage
.Account
::number
)
59 .filter(Objects
::nonNull
)
60 .collect(Collectors
.toSet());
63 public synchronized Set
<AccountsStorage
.Account
> getAllAccounts() throws IOException
{
64 return readAccounts().stream()
65 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
66 .filter(a
-> a
.number() != null)
67 .collect(Collectors
.toSet());
70 public synchronized String
getPathByNumber(String number
) throws IOException
{
71 return readAccounts().stream()
72 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
73 .filter(a
-> number
.equals(a
.number()))
74 .map(AccountsStorage
.Account
::path
)
79 public synchronized String
getPathByAci(ACI aci
) throws IOException
{
80 return readAccounts().stream()
81 .filter(a
-> a
.environment() == null || serviceEnvironment
.equals(a
.environment()))
82 .filter(a
-> aci
.toString().equals(a
.uuid()))
83 .map(AccountsStorage
.Account
::path
)
88 public synchronized void updateAccount(String path
, String number
, ACI aci
) {
89 updateAccounts(accounts
-> accounts
.stream().map(a
-> {
90 if (a
.environment() != null && !serviceEnvironment
.equals(a
.environment())) {
94 if (path
.equals(a
.path())) {
95 return new AccountsStorage
.Account(a
.path(),
98 aci
== null ?
null : aci
.toString());
101 if (number
!= null && number
.equals(a
.number())) {
102 return new AccountsStorage
.Account(a
.path(), a
.environment(), null, a
.uuid());
104 if (aci
!= null && aci
.toString().equals(a
.toString())) {
105 return new AccountsStorage
.Account(a
.path(), a
.environment(), a
.number(), null);
112 public synchronized String
addAccount(String number
, ACI aci
) {
113 final var accountPath
= generateNewAccountPath();
114 final var account
= new AccountsStorage
.Account(accountPath
,
117 aci
== null ?
null : aci
.toString());
118 updateAccounts(accounts
-> {
119 final var existingAccounts
= accounts
.stream().map(a
-> {
120 if (a
.environment() != null && !serviceEnvironment
.equals(a
.environment())) {
124 if (number
!= null && number
.equals(a
.number())) {
125 return new AccountsStorage
.Account(a
.path(), a
.environment(), null, a
.uuid());
127 if (aci
!= null && aci
.toString().equals(a
.uuid())) {
128 return new AccountsStorage
.Account(a
.path(), a
.environment(), a
.number(), null);
133 return Stream
.concat(existingAccounts
, Stream
.of(account
)).toList();
138 public void removeAccount(final String accountPath
) {
139 updateAccounts(accounts
-> accounts
.stream().filter(a
-> !(
140 (a
.environment() == null || serviceEnvironment
.equals(a
.environment())) && a
.path().equals(accountPath
)
144 private String
generateNewAccountPath() {
145 return new Random().ints(100000, 1000000)
146 .mapToObj(String
::valueOf
)
147 .filter(n
-> !new File(dataPath
, n
).exists() && !new File(dataPath
, n
+ ".d").exists())
152 private File
getAccountsFile() {
153 return new File(dataPath
, "accounts.json");
156 private void createInitialAccounts() throws IOException
{
157 final var legacyAccountPaths
= getLegacyAccountPaths();
158 final var accountsStorage
= new AccountsStorage(legacyAccountPaths
.stream()
159 .map(number
-> new AccountsStorage
.Account(number
, null, number
, null))
160 .toList(), CURRENT_STORAGE_VERSION
);
162 IOUtils
.createPrivateDirectories(dataPath
);
163 var fileName
= getAccountsFile();
164 if (!fileName
.exists()) {
165 IOUtils
.createPrivateFile(fileName
);
168 final var pair
= openFileChannel(getAccountsFile());
169 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
170 saveAccountsLocked(fileChannel
, accountsStorage
);
174 private Set
<String
> getLegacyAccountPaths() {
175 final var files
= dataPath
.listFiles();
181 return Arrays
.stream(files
)
182 .filter(File
::isFile
)
184 .filter(file
-> PhoneNumberFormatter
.isValidNumber(file
, null))
185 .collect(Collectors
.toSet());
188 private List
<AccountsStorage
.Account
> readAccounts() throws IOException
{
189 final var pair
= openFileChannel(getAccountsFile());
190 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
191 final var storage
= readAccountsLocked(fileChannel
);
193 var accountsVersion
= storage
.version() == null ?
1 : storage
.version();
194 if (accountsVersion
> CURRENT_STORAGE_VERSION
) {
195 throw new IOException("Accounts file was created by a more recent version: " + accountsVersion
);
196 } else if (accountsVersion
< MINIMUM_STORAGE_VERSION
) {
197 throw new IOException("Accounts file was created by a no longer supported older version: "
199 } else if (accountsVersion
< CURRENT_STORAGE_VERSION
) {
200 return upgradeAccountsFile(fileChannel
, storage
, accountsVersion
).accounts();
202 return storage
.accounts();
206 private AccountsStorage
upgradeAccountsFile(
207 final FileChannel fileChannel
,
208 final AccountsStorage storage
,
209 final int accountsVersion
212 List
<AccountsStorage
.Account
> newAccounts
= storage
.accounts();
213 if (accountsVersion
< 2) {
214 // add environment field
215 newAccounts
= newAccounts
.stream().map(a
-> {
216 if (a
.environment() != null) {
219 try (final var account
= accountLoader
.loadAccountOrNull(a
.path())) {
220 if (account
== null || account
.getServiceEnvironment() == null) {
223 return new AccountsStorage
.Account(a
.path(),
224 getServiceEnvironmentString(account
.getServiceEnvironment()),
230 final var newStorage
= new AccountsStorage(newAccounts
, CURRENT_STORAGE_VERSION
);
231 saveAccountsLocked(fileChannel
, newStorage
);
233 } catch (Exception e
) {
234 logger
.warn("Failed to upgrade accounts file", e
);
239 private void updateAccounts(Function
<List
<AccountsStorage
.Account
>, List
<AccountsStorage
.Account
>> updater
) {
241 final var pair
= openFileChannel(getAccountsFile());
242 try (final var fileChannel
= pair
.first(); final var lock
= pair
.second()) {
243 final var accountsStorage
= readAccountsLocked(fileChannel
);
244 final var newAccountsStorage
= updater
.apply(accountsStorage
.accounts());
245 saveAccountsLocked(fileChannel
, new AccountsStorage(newAccountsStorage
, CURRENT_STORAGE_VERSION
));
247 } catch (IOException e
) {
248 logger
.error("Failed to update accounts list", e
);
252 private AccountsStorage
readAccountsLocked(FileChannel fileChannel
) throws IOException
{
253 fileChannel
.position(0);
254 final var inputStream
= Channels
.newInputStream(fileChannel
);
255 return objectMapper
.readValue(inputStream
, AccountsStorage
.class);
258 private void saveAccountsLocked(FileChannel fileChannel
, AccountsStorage accountsStorage
) throws IOException
{
259 try (var output
= new ByteArrayOutputStream()) {
260 // Write to memory first to prevent corrupting the file in case of serialization errors
261 objectMapper
.writeValue(output
, accountsStorage
);
262 var input
= new ByteArrayInputStream(output
.toByteArray());
263 fileChannel
.position(0);
264 input
.transferTo(Channels
.newOutputStream(fileChannel
));
265 fileChannel
.truncate(fileChannel
.position());
266 fileChannel
.force(false);
270 private static Pair
<FileChannel
, FileLock
> openFileChannel(File fileName
) throws IOException
{
271 var fileChannel
= new RandomAccessFile(fileName
, "rw").getChannel();
272 var lock
= fileChannel
.tryLock();
274 logger
.info("Config file is in use by another instance, waiting…");
275 lock
= fileChannel
.lock();
276 logger
.info("Config file lock acquired.");
278 return new Pair
<>(fileChannel
, lock
);
281 private String
getServiceEnvironmentString(final ServiceEnvironment serviceEnvironment
) {
282 return switch (serviceEnvironment
) {
284 case STAGING
-> "STAGING";
288 public interface AccountLoader
{
290 SignalAccount
loadAccountOrNull(String accountPath
);