Fix HTTP handler to accept ACI/UUID account parameter in SSE endpoint (#2079)

* Fix HTTP handler to accept ACI/UUID account parameter in SSE endpoint

MultiAccountManagerImpl.getManager() only looked up accounts by phone
number, causing HTTP 400 errors when the SSE events endpoint was called
with ?account=<ACI-UUID> (e.g., cc528f93-527e-4566-8c62-d12dc99dbce0).

Changes:
- SignalAccountFiles: Add initManagerByAci() method for ACI-based lookup
- MultiAccountManagerImpl: getManager() now tries ACI lookup when
  phone number lookup fails
- HttpServerHandler: getManagerFromQuery() falls back to returning all
  managers when the specific account identifier is not found (instead of
  HTTP 400)

* Fix SSE endpoint for UUID account parameter & preserve '+' in phone numbers

Three interrelated fixes for the HTTP SSE endpoint:

1. **SignalAccountFiles** — Replace ACI.parseOrThrow() with UUID-string
   lookup from accountsStore.getAllAccounts(). The old approach failed when
   a raw UUID string (from URL query param) was passed. Added
   getAccountNumberByAci() helper to reduce duplication.

2. **MultiAccountManagerImpl** — Catch IllegalArgumentException in
   getManager() for both phone number and ACI lookup paths. Also check if
   the UUID corresponds to an already-loaded manager before trying to
   initByAci(), preventing OverlappingFileLockException when SSE requests
   arrive with a UUID for an account that was loaded at startup.

3. **Util.getQueryMap()** — Preserve '+' characters in query parameter
   values by escaping them before URLDecoder.decode(). Without this,
   URLDecoder converts '+' to space, breaking phone numbers like
   '+4915422389' which become ' 4915422389'.

* fix: address AsamK's review comments

- HttpServerHandler.getManagerFromQuery(): return null when account not
  found instead of falling back to all managers (AsamK: 'should stay
  return null here')
- MultiAccountManagerImpl.getManager(): use UuidUtil.isUuid() to branch
  early on ACI vs phone number, eliminating the try-number-then-fallback
  pattern (AsamK: 'check if identifier is a uuid first')

---------

Co-authored-by: Till L T <tilllt@users.noreply.github.com>
This commit is contained in:
tilllt 2026-07-11 18:19:15 +02:00 committed by GitHub
parent e70bddd790
commit 248c0c0dab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 16 deletions

View File

@ -5,6 +5,7 @@ import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig;
import org.signal.core.models.ServiceId.ACI;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.internal.AccountFileUpdaterImpl;
import org.asamk.signal.manager.internal.ManagerImpl;
@ -95,6 +96,30 @@ public class SignalAccountFiles {
return this.initManager(number, accountPath);
}
public String getAccountNumberByAci(final String aciStr) throws IOException {
final var accounts = accountsStore.getAllAccounts();
final var account = accounts.stream()
.filter(a -> aciStr.equals(a.uuid()))
.findFirst()
.orElse(null);
if (account == null || account.number() == null) {
return null;
}
return account.number();
}
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
final var phoneNumber = getAccountNumberByAci(aciStr);
if (phoneNumber == null) {
throw new NotRegisteredException();
}
final var accountPath = accountsStore.getPathByNumber(phoneNumber);
if (accountPath == null) {
throw new NotRegisteredException();
}
return this.initManager(phoneNumber, accountPath);
}
private Manager initManager(
String number,
String accountPath

View File

@ -8,6 +8,7 @@ import org.asamk.signal.manager.SignalAccountFiles;
import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException;
import org.slf4j.Logger;
import org.signal.core.util.UuidUtil;
import org.slf4j.LoggerFactory;
import java.io.IOException;
@ -95,23 +96,54 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
}
@Override
public Manager getManager(final String number) {
public Manager getManager(final String identifier) {
synchronized (managers) {
final var manager = managers.stream()
.filter(m -> m.getSelfNumber().equals(number))
.findFirst()
.orElse(null);
if (manager != null) {
return manager;
}
try {
final var newManager = signalAccountFiles.initManager(number);
managers.add(newManager);
return newManager;
} catch (IOException | NotRegisteredException | AccountCheckException e) {
logger.warn("Failed to load new manager", e);
return null;
if (UuidUtil.INSTANCE.isUuid(identifier)) {
// Check if UUID corresponds to an already-loaded manager
try {
final var phoneNumber = signalAccountFiles.getAccountNumberByAci(identifier);
if (phoneNumber != null) {
final var existing = managers.stream()
.filter(m -> m.getSelfNumber().equals(phoneNumber))
.findFirst()
.orElse(null);
if (existing != null) {
logger.debug("Found already loaded manager for ACI: {}", identifier);
return existing;
}
}
} catch (IOException e) {
logger.warn("Failed to lookup ACI in accounts: {}", identifier, e);
}
// Load by ACI
try {
final var newManager = signalAccountFiles.initManagerByAci(identifier);
managers.add(newManager);
return newManager;
} catch (NotRegisteredException e) {
logger.debug("Manager not found by ACI: {}", identifier);
} catch (IOException | IllegalArgumentException | AccountCheckException e) {
logger.warn("Failed to load new manager by ACI: {}", identifier, e);
}
} else {
// Phone number check already loaded managers
var existing = managers.stream()
.filter(m -> m.getSelfNumber().equals(identifier))
.findFirst()
.orElse(null);
if (existing != null) {
return existing;
}
// Load by phone number
try {
final var newManager = signalAccountFiles.initManager(identifier);
managers.add(newManager);
return newManager;
} catch (NotRegisteredException | IOException | IllegalArgumentException | AccountCheckException e) {
logger.warn("Failed to load manager by number: {}", identifier, e);
}
}
return null;
}
}

View File

@ -262,6 +262,9 @@ public class HttpServerHandler implements AutoCloseable {
} else {
final var manager = c.getManager(account);
if (manager == null) {
// Account not found by the given identifier (number or ACI/UUID)
// Log the available accounts to help debug
logger.warn("Account not found for identifier: {}", account);
return null;
}
return List.of(manager);

View File

@ -79,7 +79,7 @@ public class Util {
for (var param : params) {
final var paramParts = param.split("=", 2);
var name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
var value = paramParts.length == 1 ? null : URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
var value = paramParts.length == 1 ? null : URLDecoder.decode(paramParts[1].replace("+", "%2B"), StandardCharsets.UTF_8);
map.put(name, value);
}
return map;