From 248c0c0dab3bd1ae0ce0f688773c9ce91d051bc9 Mon Sep 17 00:00:00 2001 From: tilllt Date: Sat, 11 Jul 2026 18:19:15 +0200 Subject: [PATCH] Fix HTTP handler to accept ACI/UUID account parameter in SSE endpoint (#2079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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= (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 --- .../signal/manager/SignalAccountFiles.java | 25 ++++++++ .../internal/MultiAccountManagerImpl.java | 62 ++++++++++++++----- .../asamk/signal/http/HttpServerHandler.java | 3 + src/main/java/org/asamk/signal/util/Util.java | 2 +- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java b/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java index 82b73495..9ce9e313 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalAccountFiles.java @@ -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 diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java index 24ecae61..3adefbef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/MultiAccountManagerImpl.java @@ -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; } } diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index a6fc9f9e..c797e22a 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -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); diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 585c5690..76e41fa7 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -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;