From 3f440fe5db7bc13f97bd8e51690db04432c1eaea Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 5 Jul 2026 17:25:54 +0200 Subject: [PATCH 01/38] Removed special compile target version --- dbus-java-transport-native-unixsocket/pom.xml | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/dbus-java-transport-native-unixsocket/pom.xml b/dbus-java-transport-native-unixsocket/pom.xml index 1b615b5d8..b2c7e3260 100644 --- a/dbus-java-transport-native-unixsocket/pom.xml +++ b/dbus-java-transport-native-unixsocket/pom.xml @@ -23,26 +23,6 @@ UTC - - - - org.apache.maven.plugins - maven-compiler-plugin - - - compile - - compile - - - 16 - - - - - - - com.github.hypfvieh From aa8d1445b2699e19c4792453191e0327f627a273 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 5 Jul 2026 17:26:25 +0200 Subject: [PATCH 02/38] Addressed some code analysis findings --- .../org/freedesktop/dbus/Marshalling.java | 7 +- .../dbus/connections/AbstractConnection.java | 69 +++++++++++++++ .../freedesktop/dbus/connections/SASL.java | 42 ++++++--- .../base/AbstractConnectionBase.java | 18 ++-- .../base/ConnectionMessageHandler.java | 18 ++-- .../base/IncomingMessageThread.java | 2 +- .../connections/base/ReceivingService.java | 6 +- .../dbus/connections/impl/DBusConnection.java | 87 ++++++------------- .../impl/DBusConnectionBuilder.java | 53 ++++++----- .../connections/impl/DirectConnection.java | 32 ++----- .../transports/TransportConnection.java | 13 +-- .../freedesktop/dbus/messages/Message.java | 30 +++++-- .../freedesktop/dbus/messages/MethodCall.java | 11 ++- .../AbstractInputStreamMessageReader.java | 26 +++++- .../dbus/utils/IThrowingRunnable.java | 19 ++++ .../java/org/freedesktop/dbus/utils/Util.java | 15 ++++ .../transport/tcp/TcpTransportProvider.java | 3 +- 17 files changed, 289 insertions(+), 162 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/utils/IThrowingRunnable.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java index 547647c9a..832a20a22 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java @@ -26,6 +26,8 @@ public final class Marshalling { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; + private static final int MAXIMUM_RECURSION_DEPTH = 32; + /** Used as initial and incremental size of StringBuffer array when resolving DBusTypes recursively. */ private static final int INITIAL_BUFFER_SZ = 10; @@ -188,6 +190,9 @@ public static String[] getDBusType(Type _dataType, boolean _basic) throws DBusEx @SuppressWarnings("checkstyle:parameterassignment") private static String[] recursiveGetDBusType(StringBuffer[] _out, Type _dataType, boolean _basic, int _level) throws DBusException { + if (_level > MAXIMUM_RECURSION_DEPTH) { + throw new DBusException("Maximum recursion depth exceeded"); + } if (_out.length <= _level) { StringBuffer[] newout = new StringBuffer[_level + INITIAL_BUFFER_SZ]; System.arraycopy(_out, 0, newout, 0, _out.length); @@ -368,7 +373,7 @@ private static String[] recursiveGetDBusType(StringBuffer[] _out, Type _dataType * @throws DBusException on error */ public static int getJavaType(String _dbusType, List _resultValue, int _limit) throws DBusException { - if (null == _dbusType || _dbusType.isEmpty() || 0 == _limit) { + if (null == _dbusType || _dbusType.isEmpty() || 0 == _limit || _limit > MAXIMUM_RECURSION_DEPTH) { return 0; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index 6cfdea153..e83f6d259 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -1,5 +1,7 @@ package org.freedesktop.dbus.connections; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.freedesktop.dbus.DBusAsyncReply; import org.freedesktop.dbus.RemoteInvocationHandler; import org.freedesktop.dbus.RemoteObject; @@ -24,6 +26,7 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.regex.Pattern; +import org.freedesktop.dbus.utils.IThrowingRunnable; /** * Handles a connection to DBus. @@ -99,6 +102,72 @@ protected IncomingMessageThread createReaderThread(BusAddress _busAddress) { */ protected abstract AutoCloseable addGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException; + /** + * Removes a signal handler from the signal map based on the specified match rule. + * If the queue associated with the match rule becomes empty after removal, the match rule + * is removed from the map, and the provided callback is executed. + * + * @param _map The signal map containing match rules mapped to queues of signal handlers. + * @param _rule The match rule used to locate the corresponding queue in the signal map. + * @param _handler The signal handler to remove from the queue associated with the match rule. + * @param _onEmpty A callback to execute if the queue associated with the match rule becomes + * empty after removing the handler. Can be null. + * @throws DBusException If an error occurs during the execution of the callback when the queue + * is empty. + */ + protected > void removeFromSignalMap( + Map> _map, DBusMatchRule _rule, H _handler, IThrowingRunnable _onEmpty) throws DBusException { + + synchronized (_map) { + Queue queue = _map.get(_rule); + if (queue != null) { + queue.remove(_handler); + if (queue.isEmpty()) { + _map.remove(_rule); + if (_onEmpty != null) { + _onEmpty.run(); + } + } + } + } + } + + /** + * Adds a signal handler to the signal map for a given match rule. If the match rule + * is newly added to the map, an optional runnable action is executed. + * + * @param The type of the signal handler, extending {@link DBusSigHandler}. + * @param _map The map storing the match rules and their corresponding handler queues. + * @param _rule The match rule that defines the criteria for the signal. + * @param _handler The signal handler to be added to the queue associated with the match rule. + * @param _onNew An optional action to execute if the match rule is newly added to the map. + * This action may throw a {@link DBusException}. + * @throws DBusException If the optional action provided by _onNew encounters an exception. + */ + protected > void addToSignalMap(Map> _map, DBusMatchRule _rule, H _handler, + IThrowingRunnable _onNew) throws DBusException { + + synchronized (_map) { + AtomicBoolean isNew = new AtomicBoolean(false); + + Queue queue = _map.computeIfAbsent(_rule, v -> { + isNew.set(true); + return new ConcurrentLinkedQueue<>(); + }); + + queue.add(_handler); + + if (_onNew != null && isNew.get()) { + try { + _onNew.run(); + } catch (DBusException _ex) { + queue.remove(_handler); + throw _ex; + } + } + } + } + /** * If given type is null, will try to find suitable types by examining the given ifaces. * If a non-null type is given, returns the given type. diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index f564b62bd..7fe560faa 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -3,6 +3,7 @@ import static org.freedesktop.dbus.connections.SASL.SaslCommand.*; import com.sun.security.auth.module.UnixSystem; +import java.nio.file.StandardCopyOption; import org.freedesktop.dbus.config.DBusSysProps; import org.freedesktop.dbus.connections.config.SaslConfig; import org.freedesktop.dbus.connections.transports.AbstractTransport; @@ -75,9 +76,12 @@ public class SASL { private String cookie = ""; private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Random secureRandom = new SecureRandom(); + + private final SaslConfig saslConfig; + /** whether file descriptor passing is supported on the current connection. */ private boolean fileDescriptorSupported; - private final SaslConfig saslConfig; /** * Create a new SASL auth handler. @@ -164,10 +168,14 @@ private void addCookie(String _context, String _id, long _timestamp, String _coo String s = null; while (null != (s = r.readLine())) { String[] line = s.split(" "); - long time = Long.parseLong(line[1]); - // expire stale cookies - if ((_timestamp - time) < COOKIE_TIMEOUT) { - lines.add(s); + try { + long time = Long.parseLong(line[1]); + // expire stale cookies + if ((_timestamp - time) < COOKIE_TIMEOUT) { + lines.add(s); + } + } catch (NumberFormatException _ex) { + logger.warn("Ignoring malformed cookie line {}", s); } } } @@ -181,14 +189,10 @@ private void addCookie(String _context, String _id, long _timestamp, String _coo StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); // atomically move to old file - if (!temp.renameTo(cookiefile)) { - if (!cookiefile.delete()) { - logger.warn("Unable to delete cookie file {}", cookiefile); - } else { - if (!temp.renameTo(cookiefile)) { - logger.warn("Unable to rename cookie file {} to {}", temp, cookiefile); - } - } + try { + Files.move(temp.toPath(), cookiefile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + logger.warn("Unable to atomically move cookie file {} to {}", temp, cookiefile); } // remove lock @@ -313,7 +317,7 @@ SaslResult doChallenge(int _auth, SASL.Command _c) throws IOException { byte[] buf = new byte[8]; // ensure we get a (more or less unique) positive long - long seed = Optional.of(System.nanoTime()).map(t -> t < 0 ? t * -1 : t).get(); + long seed = secureRandom.nextLong(0, Long.MAX_VALUE); Message.marshallintBig(seed, buf, 0, 8); String clientchallenge = stupidlyEncode(md.digest(buf)); @@ -324,6 +328,13 @@ SaslResult doChallenge(int _auth, SASL.Command _c) throws IOException { while (lCookie == null && tm.getElapsed() < LOCK_TIMEOUT) { lCookie = findCookie(context, id); + if (lCookie == null) { + try { + Thread.sleep(100); + } catch (InterruptedException _ex) { + Thread.currentThread().interrupt(); + } + } } if (lCookie == null) { @@ -605,6 +616,9 @@ public boolean auth(SocketChannel _sock, AbstractTransport _transport) throws IO } if (kuid >= 0) { kernelUid = stupidlyEncode("" + kuid); + } else { + state = SaslAuthState.FAILED; + break; } state = SaslAuthState.WAIT_AUTH; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index e8e04d738..1da48777c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -221,14 +221,18 @@ protected final synchronized void internalDisconnect(IOException _connectionErro receivingService.shutdown(10, TimeUnit.SECONDS); // stop potentially waiting method-calls - getLogger().debug("Notifying {} method call(s) to stop waiting for replies", getPendingCalls().size()); Exception interrupt = _connectionError == null ? new IOException("Disconnecting") : _connectionError; - for (MethodCall mthCall : getPendingCalls().values()) { - try { - mthCall.setReply(getMessageFactory().createError(mthCall, interrupt)); - } catch (DBusException _ex) { - getLogger().debug("Cannot set method reply to error", _ex); + + synchronized (getPendingCalls()) { + getLogger().debug("Notifying {} method call(s) to stop waiting for replies", getPendingCalls().size()); + for (MethodCall mthCall : getPendingCalls().values()) { + try { + mthCall.setReply(getMessageFactory().createError(mthCall, interrupt)); + } catch (DBusException _ex) { + getLogger().debug("Cannot set method reply to error", _ex); + } } + getPendingCalls().clear(); } // shutdown sender executor service, send all remaining messages in main thread when no exception caused disconnection @@ -356,7 +360,7 @@ public BusAddress getAddress() { * * @return true if connected */ - public boolean isConnected() { + public synchronized boolean isConnected() { return transport != null && transport.isConnected(); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index ac7f3dbf9..42edd7652 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -150,8 +150,11 @@ public synchronized void run() { DBusCallInfo info = new DBusCallInfo(_err); getInfoMap().put(Thread.currentThread(), info); - fcbh.handleError(_err.getException()); - getInfoMap().remove(Thread.currentThread()); + try { + fcbh.handleError(_err.getException()); + } finally { + getInfoMap().remove(Thread.currentThread()); + } } catch (Exception _ex) { getLogger().debug("Exception while running error callback.", _ex); @@ -206,10 +209,13 @@ public synchronized void run() { getLogger().trace("Running Callback for {}", _mr); DBusCallInfo info = new DBusCallInfo(_mr); getInfoMap().put(Thread.currentThread(), info); - Object convertRV = RemoteInvocationHandler.convertRV(_mr.getParameters(), fasr.getMethod(), - fasr.getConnection()); - fcbh.handle(convertRV); - getInfoMap().remove(Thread.currentThread()); + try { + Object convertRV = RemoteInvocationHandler.convertRV(_mr.getParameters(), fasr.getMethod(), + fasr.getConnection()); + fcbh.handle(convertRV); + } finally { + getInfoMap().remove(Thread.currentThread()); + } } catch (Exception _ex) { getLogger().debug("Exception while running callback.", _ex); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java index fdac91b43..1037ac761 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java @@ -42,7 +42,7 @@ public void run() { connection.handleMessage(msg); } - } catch (DBusException | RejectedExecutionException | IllegalThreadPoolStateException _ex) { + } catch (DBusException | RuntimeException _ex) { if (_ex instanceof FatalException) { if (terminate) { // requested termination, ignore failures return; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java index c38934b76..d07fe6377 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java @@ -42,10 +42,8 @@ public class ReceivingService { ReceivingServiceConfig rsCfg = Optional.ofNullable(_rsCfg).orElse(ReceivingServiceConfigBuilder.getDefaultConfig()); Arrays.stream(ExecutorNames.values()) - .forEach(t -> { - executors.put(t, - Executors.newFixedThreadPool(rsCfg.getPoolSize(t), createFactory(prefix + t.getThreadName() + "-", _rsCfg.isVirtual(t), _rsCfg.getPriority(t)))); - }); + .forEach(t -> executors.put(t, + Executors.newFixedThreadPool(rsCfg.getPoolSize(t), createFactory(prefix + t.getThreadName() + "-", rsCfg.isVirtual(t), rsCfg.getPriority(t))))); retryHandler = rsCfg.getRetryHandler(); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java index 12ff3cd9a..99a3d5632 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java @@ -392,29 +392,20 @@ public void removeSigHandler(Class _type, String _sour removeSigHandler(DBusMatchRuleBuilder.create().withType(_type).withSender(_source).withPath(objectPath).build(), _handler); } - /** - * {@inheritDoc} - */ @Override public void removeSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> dbusSignalList = getHandledSignals().get(_rule); - - if (null != dbusSignalList) { - dbusSignalList.remove(_handler); - if (dbusSignalList.isEmpty()) { - getHandledSignals().remove(_rule); - try { - dbus.RemoveMatch(_rule.toString()); - } catch (NotConnected _ex) { - logger.debug("No connection.", _ex); - } catch (DBusExecutionException _ex) { - logger.debug("Error removing signal", _ex); - throw new DBusException(_ex); - } + removeFromSignalMap(getHandledSignals(), _rule, _handler, () -> { + try { + dbus.RemoveMatch(_rule.toString()); + } catch (NotConnected _ex) { + logger.debug("No connection", _ex); + } catch (DBusExecutionException _ex) { + logger.debug("Error removing signal", _ex); + throw new DBusException(_ex); } - } + }); } /** @@ -485,27 +476,15 @@ public AutoCloseable addSigHandler(DBusMatchRule _rule, D Objects.requireNonNull(_rule, "Match rule cannot be null"); Objects.requireNonNull(_handler, "Handler cannot be null"); - AtomicBoolean addMatch = new AtomicBoolean(false); // flag to perform action if this is a new signal key - - Queue> dbusSignalList = - getHandledSignals().computeIfAbsent(_rule, v -> { - Queue> signalList = new ConcurrentLinkedQueue<>(); - addMatch.set(true); - return signalList; - }); - - // add handler to signal list - dbusSignalList.add(_handler); - - // add match rule if this rule is new - if (addMatch.get()) { + addToSignalMap(getHandledSignals(), _rule, _handler, () -> { try { dbus.AddMatch(_rule.toString()); } catch (DBusExecutionException _ex) { logger.debug("Cannot add match rule: {}", _rule, _ex); - throw new DBusException("Cannot add match rule.", _ex); + throw new DBusException("Cannot add match rule", _ex); } - } + }); + return () -> removeSigHandler(_rule, _handler); } @@ -592,45 +571,29 @@ public String getMachineId() { @Override public void removeGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> genericSignalsList = getGenericHandledSignals().get(_rule); - if (null != genericSignalsList) { - genericSignalsList.remove(_handler); - if (genericSignalsList.isEmpty()) { - getGenericHandledSignals().remove(_rule); - try { - dbus.RemoveMatch(_rule.toString()); - } catch (NotConnected _ex) { - logger.debug("No connection.", _ex); - } catch (DBusExecutionException _ex) { - logger.debug("Error removing generic signal", _ex); - throw new DBusException(_ex); - } + removeFromSignalMap(getGenericHandledSignals(), _rule, _handler, () -> { + try { + dbus.RemoveMatch(_rule.toString()); + } catch (NotConnected _ex) { + logger.debug("No connection", _ex); + } catch (DBusExecutionException _ex) { + logger.debug("Error removing generic signal", _ex); + throw new DBusException(_ex); } - } + }); } @Override public AutoCloseable addGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - AtomicBoolean addMatch = new AtomicBoolean(false); // flag to perform action if this is a new signal key - - Queue> genericSignalsList = - getGenericHandledSignals().computeIfAbsent(_rule, v -> { - Queue> signalsList = new ConcurrentLinkedQueue<>(); - addMatch.set(true); - - return signalsList; - }); - - genericSignalsList.add(_handler); - - if (addMatch.get()) { + addToSignalMap(getGenericHandledSignals(), _rule, _handler, () -> { try { dbus.AddMatch(_rule.toString()); } catch (DBusExecutionException _ex) { logger.debug("Error adding signal handler", _ex); throw new DBusException(_ex.getMessage()); } - } + }); + return () -> removeGenericSigHandler(_rule, _handler); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java index 43f4e5a55..b24b4d795 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java @@ -2,6 +2,7 @@ import static org.freedesktop.dbus.utils.AddressBuilder.getDbusMachineId; +import java.util.Optional; import org.freedesktop.dbus.connections.BusAddress; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; @@ -122,23 +123,25 @@ private static BusAddress validateTransportAddress(BusAddress _address) { } // no unix transport but address wants to use a unix socket - if (!TransportBuilder.getRegisteredBusTypes().contains("UNIX") - && _address != null - && _address.isBusType("UNIX")) { - throw new AddressResolvingException("No transports found to handle UNIX socket connections. Please add a unix-socket transport provider to your classpath"); - } + validateTransportAvailable("UNIX", _address, + "No transports found to handle UNIX socket connections. Please add a unix-socket transport provider to your classpath"); // no tcp transport but TCP address given - if (!TransportBuilder.getRegisteredBusTypes().contains("TCP") - && _address != null - && _address.isBusType("TCP")) { - throw new AddressResolvingException("No transports found to handle TCP connections. Please add a TCP transport provider to your classpath"); - } + validateTransportAvailable("TCP", _address, + "No transports found to handle TCP connections. Please add a TCP transport provider to your classpath"); return _address; } + private static void validateTransportAvailable(String _type, BusAddress _address, String _errorMessage) { + if (!TransportBuilder.getRegisteredBusTypes().contains(_type) + && _address != null + && _address.isBusType(_type)) { + throw new AddressResolvingException(_errorMessage); + } + } + /** * Use this connection as shared connection. Shared connection means that the same connection is used multiple times * if the connection parameter did not change. Default is true. @@ -176,10 +179,23 @@ public DBusConnection build() throws DBusException { } } } else { - c = new DBusConnection(shared, machineId, connectionConfig, transportCfg, rcvSvcCfg); + c = new DBusConnection(false, machineId, connectionConfig, transportCfg, rcvSvcCfg); } - c.connectImpl(); + try { + c.connectImpl(); + } catch (DBusException _ex) { + if (shared) { + // remove shared connection if connection failed + synchronized (DBusConnection.CONNECTIONS) { + DBusConnection removedConnection = DBusConnection.CONNECTIONS.remove(transportCfg.getBusAddress().toString()); + if (removedConnection != null) { + removedConnection.close(); + } + } + } + throw _ex; + } return c; } @@ -193,15 +209,12 @@ public DBusConnection build() throws DBusException { private DBusConnection getSharedConnection(String _busAddr) { synchronized (DBusConnection.CONNECTIONS) { DBusConnection c = DBusConnection.CONNECTIONS.get(_busAddr); - if (c != null) { - if (!c.isConnected()) { - DBusConnection.CONNECTIONS.remove(_busAddr); - return null; - } else { - return c; - } + if (c != null && !c.isConnected()) { + Optional.ofNullable(DBusConnection.CONNECTIONS.remove(_busAddr)) + .ifPresent(DBusConnection::close); + return null; } + return c; } - return null; } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java index 53af7cf10..a18f325c0 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java @@ -170,41 +170,23 @@ public T getRemoteObject(String _objectPath, Class @Override public void removeSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> v = getHandledSignals().get(_rule); - if (v != null) { - v.remove(_handler); - if (v.isEmpty()) { - getHandledSignals().remove(_rule); - } - } + removeFromSignalMap(getHandledSignals(), _rule, _handler, () -> getHandledSignals().remove(_rule)); } @Override - public AutoCloseable addSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> v = - getHandledSignals().computeIfAbsent(_rule, val -> new ConcurrentLinkedQueue<>()); - - v.add(_handler); - return () -> removeSigHandler(_rule, _handler); + protected void removeGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { + removeFromSignalMap(getGenericHandledSignals(), _rule, _handler, () -> getGenericHandledSignals().remove(_rule)); } @Override - protected void removeGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> v = getGenericHandledSignals().get(_rule); - if (v != null) { - v.remove(_handler); - if (v.isEmpty()) { - getGenericHandledSignals().remove(_rule); - } - } + public AutoCloseable addSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { + addToSignalMap(getHandledSignals(), _rule, _handler, () -> {}); + return () -> removeSigHandler(_rule, _handler); } @Override protected AutoCloseable addGenericSigHandler(DBusMatchRule _rule, DBusSigHandler _handler) throws DBusException { - Queue> v = - getGenericHandledSignals().computeIfAbsent(_rule, val -> new ConcurrentLinkedQueue<>()); - - v.add(_handler); + addToSignalMap(getGenericHandledSignals(), _rule, _handler, () -> {}); return () -> removeGenericSigHandler(_rule, _handler); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java index 88945b6a1..6158a0bd2 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.channels.SocketChannel; import java.util.concurrent.atomic.AtomicLong; +import org.freedesktop.dbus.utils.Util; /** * Represents one transport connection of any type.
@@ -71,17 +72,7 @@ public String toString() { @Override public void close() throws IOException { - if (reader != null) { - reader.close(); - } - - if (writer != null) { - writer.close(); - } - - if (channel != null) { - channel.close(); - } + Util.closeQuietly(reader, writer, channel); } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java index e42e9a007..b288d84f7 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java @@ -454,7 +454,7 @@ public String toString() { * @return The value of the field or null if unset. */ protected Object getHeader(byte _type) { - return headers.length == 0 || headers.length < _type ? null : headers[_type]; + return headers.length == 0 || headers.length <= _type ? null : headers[_type]; } /** @@ -924,20 +924,20 @@ private Object extractOne(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, _offsets[OFFSET_DATA] += 4; break; case STRING: - int length = (int) demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4); + int length = validateLengthLimit(demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4), _dataBuf.length); _offsets[OFFSET_DATA] += 4; rv = new String(_dataBuf, _offsets[OFFSET_DATA], length, StandardCharsets.UTF_8); _offsets[OFFSET_DATA] += length + 1; break; case OBJECT_PATH: - length = (int) demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4); + length = validateLengthLimit(demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4), _dataBuf.length); _offsets[OFFSET_DATA] += 4; - rv = new DBusPath(getSource(), new String(_dataBuf, _offsets[OFFSET_DATA], length)); + rv = new DBusPath(getSource(), new String(_dataBuf, _offsets[OFFSET_DATA], length, StandardCharsets.UTF_8)); _offsets[OFFSET_DATA] += length + 1; break; case SIGNATURE: - length = _dataBuf[_offsets[OFFSET_DATA]++] & 0xFF; - rv = new String(_dataBuf, _offsets[OFFSET_DATA], length); + length = validateLengthLimit(_dataBuf[_offsets[OFFSET_DATA]++] & 0xFF, _dataBuf.length); + rv = new String(_dataBuf, _offsets[OFFSET_DATA], length, StandardCharsets.UTF_8); _offsets[OFFSET_DATA] += length + 1; break; default: @@ -955,6 +955,24 @@ private Object extractOne(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, return rv; } + /** + * Validates that the provided length is within the bounds of the buffer. + * @param _length length to validate + * @param _bufferLen length of the buffer + * @return validated length as integer + * @throws MessageFormatException when the length is out of bounds + */ + private int validateLengthLimit(long _length, int _bufferLen) throws MessageFormatException { + if (_length > Integer.MAX_VALUE) { + throw new MessageFormatException("Length limit exceeded: " + _length); + } else if (_length < 0) { + throw new MessageFormatException("Invalid length: " + _length); + } else if (_bufferLen < _length) { + throw new MessageFormatException("Length of " + _length + " exceeds buffer size"); + } + return (int) _length; + } + /** * Extracts a byte from the data received on bus. * diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java index b9a8245cb..79727a260 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java @@ -1,5 +1,6 @@ package org.freedesktop.dbus.messages; +import java.util.concurrent.TimeUnit; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.MessageFormatException; import org.freedesktop.dbus.messages.constants.ArgumentType; @@ -86,9 +87,15 @@ public synchronized Message getReply(long _timeout) { return reply; } + long remainingNanos = TimeUnit.MILLISECONDS.toNanos(_timeout); + long deadline = System.nanoTime() + remainingNanos; + try { - wait(_timeout); - } catch (InterruptedException _exI) { + while (null == reply && remainingNanos > 0) { + TimeUnit.NANOSECONDS.timedWait(this, remainingNanos); + remainingNanos = deadline - System.nanoTime(); + } + } catch (InterruptedException _ex) { Thread.currentThread().interrupt(); // keep interrupted state } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java index 36b9461d3..b122cbc8b 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java @@ -102,7 +102,7 @@ public final Message readMessage() throws IOException, DBusException { int headerlen; if (header == null) { - headerlen = (int) Message.demarshallint(tbuf, 0, endian, 4); + headerlen = demarshallLength(tbuf, 0, endian, 4); /* n % 2^i = n & (2^i - 1) */ final int modlen = headerlen & 7; @@ -144,7 +144,7 @@ public final Message readMessage() throws IOException, DBusException { /* Read the body */ if (body == null) { - body = new byte[(int) Message.demarshallint(buf, 4, endian, 4)]; + body = new byte[demarshallLength(buf, 4, endian, 4)]; len[3] = 0; } @@ -230,4 +230,26 @@ public String toString() { return getClass().getSimpleName() + " [inputChannel=" + inputChannel + ", socketProviderImpl=" + socketProviderImpl + "]"; } + /** + * Extracts the length from a portion of a byte array, validates it, + * and returns the length as an integer. + * + * @param _buf the byte array containing the length to be extracted + * @param _ofs the offset in the array where the length starts + * @param _endian the endianness of the data + * @param _width the width (in bytes) of the length field + * @return the validated length as an integer + * @throws DBusException if the extracted length exceeds the maximum allowed length + * or if the length is less than or equal to 0 + */ + private int demarshallLength(byte[] _buf, int _ofs, byte _endian, int _width) throws DBusException { + // Message.demarshallint(tbuf, 0, endian, 4) + long length = Message.demarshallint(_buf, _ofs, _endian, _width); + if (length > Message.MAXIMUM_MESSAGE_LENGTH) { + throw new DBusException("Message length exceeds maximum allowed length"); + } else if (length <= 0) { + throw new DBusException("Message length must be greater than 0"); + } + return (int) length; + } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/IThrowingRunnable.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/IThrowingRunnable.java new file mode 100644 index 000000000..6b50b00f5 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/IThrowingRunnable.java @@ -0,0 +1,19 @@ +package org.freedesktop.dbus.utils; + +/** + * Runnable which allows throwing any exception. + * + * @param type of exception which gets thrown + * + * @author hypfvieh + * @since v6.0.0 - 2026-07-05 + */ +@FunctionalInterface +public interface IThrowingRunnable { + /** + * Returns the result of the supplier or throws an exception. + * + * @throws T exception + */ + void run() throws T; +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java index a32478119..adc03e7ac 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java @@ -738,4 +738,19 @@ public static int requireMinimum(int _minimum, int _checkVal) { return _checkVal; } + /** + * Close the given closeables quietly. + * @param _closeables closeables to close + */ + public static void closeQuietly(Closeable... _closeables) { + for (AutoCloseable c : _closeables) { + if (c != null) { + try { + c.close(); + } catch (Exception e) { + LOGGER.debug("Failed to close {}", c, e); + } + } + } + } } diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java index a8b57310f..52112e141 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java @@ -1,5 +1,6 @@ package org.freedesktop.dbus.transport.tcp; +import java.security.SecureRandom; import org.freedesktop.dbus.connections.BusAddress; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractTransport; @@ -14,7 +15,7 @@ public class TcpTransportProvider implements ITransportProvider { public static final int TCP_CONNECT_TIMEOUT = 100000; - private static final Random RANDOM = new Random(); + private static final Random RANDOM = new SecureRandom(); @Override public String getTransportName() { From 7854257d238f3df3ca593ec0eeafb3e83789edec Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 5 Jul 2026 17:40:30 +0200 Subject: [PATCH 03/38] More code review findings --- .../dbus/utils/AddressBuilder.java | 2 +- .../org/freedesktop/dbus/utils/Hexdump.java | 2 +- .../java/org/freedesktop/dbus/utils/Util.java | 12 +-- .../generator/InterfaceCodeGenerator.java | 102 +++++++++--------- .../generator/type/ClassBuilderInfo.java | 2 +- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java index b80f37880..ccbd4b72b 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java @@ -47,7 +47,7 @@ public static BusAddress getSessionConnection(String _dbusMachineIdFile) { // no session address in process properties, try to get it from environment if (s == null) { // MacOS support: e.g DBUS_LAUNCHD_SESSION_BUS_SOCKET=/private/tmp/com.apple.launchd.4ojrKe6laI/unix_domain_listener - if (Util.isMacOs()) { + if (Util.isMacOs() && System.getenv(DBusSysProps.DBUS_SESSION_BUS_ADDRESS_MACOS) != null) { s = "unix:path=" + System.getenv(DBusSysProps.DBUS_SESSION_BUS_ADDRESS_MACOS); } else { // all others (linux) s = System.getenv(DBusSysProps.DBUS_SESSION_BUS_ADDRESS); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Hexdump.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Hexdump.java index 2468728b1..b99c8546f 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Hexdump.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Hexdump.java @@ -49,7 +49,7 @@ public static String toAscii(byte[] _buf, int _ofs, int _len) { int j = _ofs + _len; for (int i = _ofs; i < j; i++) { if (i < _buf.length) { - if (20 <= _buf[i] && 126 >= _buf[i]) { + if (0x20 <= _buf[i] && 126 >= _buf[i]) { sb.append((char) _buf[i]); } else { sb.append('.'); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java index adc03e7ac..dbd14a407 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java @@ -62,9 +62,9 @@ private Util() {} */ public static Properties readProperties(File _file) { if (_file.exists()) { - try { - return readProperties(new FileInputStream(_file)); - } catch (FileNotFoundException _ex) { + try (FileInputStream fis = new FileInputStream(_file)){ + return readProperties(fis); + } catch (IOException _ex) { LOGGER.info("Could not load properties file: {}", _file, _ex); } } @@ -295,7 +295,7 @@ public static String readFileToString(File _file) { */ public static List getTextfileFromUrl(String _url, Charset _charset, boolean _silent) { if (_url == null) { - return null; + return List.of(); } String fileUrl = _url; if (!fileUrl.contains("://")) { @@ -426,7 +426,7 @@ public static boolean collectionContainsAny(Collection _haystack, Collect public static String getCurrentUser() { String[] sysPropParms = new String[] {"user.name", "USER", "USERNAME"}; for (String sysPropParm : sysPropParms) { - String val = System.getProperty(sysPropParm); + String val = System.getenv(sysPropParm); if (!isEmpty(val)) { return val; } @@ -492,7 +492,7 @@ public static String createDynamicSessionAddress(boolean _listeningSocket, boole do { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10; i++) { - sb.append((char) (Math.abs(RANDOM.nextInt(0, Integer.MAX_VALUE)) % 26) + 65); + sb.append((char) ((Math.abs(RANDOM.nextInt(0, Integer.MAX_VALUE)) % 26) + 65)); } path = path.replaceAll("..........$", sb.toString()); LoggerFactory.getLogger(Util.class).trace("Trying path {}", path); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index 2d06dc058..b8803db34 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -229,7 +229,7 @@ private List extractSignals(Element _signalElement, ClassBuild String className = _signalElement.getAttribute("name"); if (className.contains(".")) { - className = className.substring(className.lastIndexOf('.')); + className = className.substring(className.lastIndexOf('.') + 1); } ClassBuilderInfo innerClass = new ClassBuilderInfo(argumentPrefix); @@ -679,60 +679,62 @@ public static void main(String[] _args) { for (int i = 0; i < _args.length; i++) { String p = _args[i]; - if ("--system".equals(p) || "-y".equals(p)) { - busType = DBusBusType.SYSTEM; - } else if ("--session".equals(p) || "-s".equals(p)) { - busType = DBusBusType.SESSION; - } else if ("--enable-dtd-validation".equals(p)) { - ignoreDtd = false; - } else if ("--help".equals(p) || "-h".equals(p)) { - printHelp(); - System.exit(0); - } else if ("--all".equals(p) || "-a".equals(p)) { - noFilter = true; - } else if ("--argumentPrefix".equals(p)) { - if (_args.length > i) { - argumentPrefix = _args[++i]; - } else { + switch (p) { + case "--system", "-y" -> busType = DBusBusType.SYSTEM; + case "--session", "-s" -> busType = DBusBusType.SESSION; + case "--enable-dtd-validation" -> ignoreDtd = false; + case "--help", "-h" -> { printHelp(); System.exit(0); } - } else if ("--propertyMethods".equals(p) || "-m".equals(p)) { - propertyMethods = true; - } else if ("--disable-tuples".equals(p) || "-t".equals(p)) { - disableTuples = true; - } else if ("--package".equals(p) || "-p".equals(p)) { - if (_args.length > i) { - forcePackageName = _args[++i]; - } else { - printHelp(); - System.exit(0); + case "--all", "-a" -> noFilter = true; + case "--argumentPrefix" -> { + if (_args.length > i + 1) { + argumentPrefix = _args[++i]; + } else { + printHelp(); + System.exit(0); + } } - } else if ("--version".equals(p) || "-v".equals(p)) { - version(); - System.exit(0); - } else if ("--outputDir".equals(p) || "-o".equals(p)) { - if (_args.length > i) { - outputDir = _args[++i]; - } else { - printHelp(); - System.exit(0); + case "--propertyMethods", "-m" -> propertyMethods = true; + case "--disable-tuples", "-t" -> disableTuples = true; + case "--package", "-p" -> { + if (_args.length > i + 1) { + forcePackageName = _args[++i]; + } else { + printHelp(); + System.exit(0); + } } - } else if ("--inputFile".equals(p) || "-i".equals(p)) { - if (_args.length > i) { - inputFile = _args[++i]; - } else { - printHelp(); + case "--version", "-v" -> { + version(); System.exit(0); } - } else { - if (null == busName) { - busName = p; - } else if (null == objectPath) { - objectPath = p; - } else { - printHelp(); - System.exit(1); + case "--outputDir", "-o" -> { + if (_args.length > i + 1) { + outputDir = _args[++i]; + } else { + printHelp(); + System.exit(0); + } + } + case "--inputFile", "-i" -> { + if (_args.length > i + 1) { + inputFile = _args[++i]; + } else { + printHelp(); + System.exit(0); + } + } + case null, default -> { + if (null == busName) { + busName = p; + } else if (null == objectPath) { + objectPath = p; + } else { + printHelp(); + System.exit(1); + } } } } @@ -818,10 +820,10 @@ private static void printHelp() { System.out.println(" --system | -y Use SYSTEM DBus"); System.out.println(" --session | -s Use SESSION DBus"); System.out.println(" --outputDir | -o Use as output directory for all generated files"); - System.out.println(" --packageName | -p Use as the Java package instead of using the DBus namespace."); + System.out.println(" --package | -p Use as the Java package instead of using the DBus namespace."); System.out.println(" --inputFile | -i Use as XML introspection input file instead of querying DBus"); System.out.println(" --all | -a Create all classes for given bus name (do not filter)"); - System.out.println(" --boundProperties | -b Generate setter/getter methods for properties"); + System.out.println(" --propertyMethods | -m Generate setter/getter methods for properties"); System.out.println(); System.out.println(" --disable-tuples | -t Create Struct based classes for multi-value " + "return methods instead of creating Tuple classes (code will only work with dbus-java 5.2.0+)"); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java index dd415b8f5..8fc25ab30 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java @@ -205,7 +205,7 @@ private List createClassFileContent(boolean _staticClass, Set _o String annotationCode = classIndent + "@" + annotation.getAnnotationClass().getSimpleName(); if (annotation.getAnnotationParams() != null) { - annotationCode += "(" + annotation.getAnnotationParams() + ")"; + annotationCode += "(" + annotation.getAnnotationString() + ")"; } content.add(annotationCode); } From f59adcc22c5bc202ed8e122de792065ccb512e6b Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 5 Jul 2026 18:27:30 +0200 Subject: [PATCH 04/38] Regression fixes --- .../org/freedesktop/dbus/Marshalling.java | 18 ++++++--- .../dbus/connections/AbstractConnection.java | 5 +++ .../base/IncomingMessageThread.java | 39 ++++++++++++------- .../freedesktop/dbus/messages/Message.java | 15 ++++--- .../freedesktop/dbus/messages/MethodCall.java | 19 +++++---- .../AbstractInputStreamMessageReader.java | 7 ++-- 6 files changed, 67 insertions(+), 36 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java index 832a20a22..14cf8eea4 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java @@ -373,10 +373,18 @@ private static String[] recursiveGetDBusType(StringBuffer[] _out, Type _dataType * @throws DBusException on error */ public static int getJavaType(String _dbusType, List _resultValue, int _limit) throws DBusException { - if (null == _dbusType || _dbusType.isEmpty() || 0 == _limit || _limit > MAXIMUM_RECURSION_DEPTH) { + return getJavaType(_dbusType, _resultValue, _limit, 0); + } + + private static int getJavaType(String _dbusType, List _resultValue, int _limit, int _depth) throws DBusException { + if (null == _dbusType || _dbusType.isEmpty() || 0 == _limit) { return 0; } + if (_depth > MAXIMUM_RECURSION_DEPTH) { + throw new DBusException("Maximum recursion depth exceeded parsing DBus type signature: " + _dbusType); + } + try { int idx = 0; for (; idx < _dbusType.length() && (-1 == _limit || _limit > _resultValue.size()); idx++) { @@ -392,19 +400,19 @@ public static int getJavaType(String _dbusType, List _resultValue, int _li } List contained = new ArrayList<>(); - getJavaType(_dbusType.substring(idx + 1, structIdx - 1), contained, -1); + getJavaType(_dbusType.substring(idx + 1, structIdx - 1), contained, -1, _depth + 1); _resultValue.add(new DBusStructType(contained.toArray(EMPTY_TYPE_ARRAY))); idx = structIdx - 1; //-1 because j already points to the next signature char break; case ArgumentType.ARRAY: if (ArgumentType.DICT_ENTRY1 == _dbusType.charAt(idx + 1)) { contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 2), contained, 2); + int javaType = getJavaType(_dbusType.substring(idx + 2), contained, 2, _depth + 1); _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); idx += javaType + 2; } else { contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 1); + int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 1, _depth + 1); _resultValue.add(new DBusListType(contained.getFirst())); idx += javaType; } @@ -456,7 +464,7 @@ public static int getJavaType(String _dbusType, List _resultValue, int _li break; case ArgumentType.DICT_ENTRY1: contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 2); + int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 2, _depth + 1); _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); idx += javaType + 1; break; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index e83f6d259..d0b9d9ccd 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -161,7 +161,12 @@ protected > void addToSignalMap(Map Integer.MAX_VALUE) { throw new MessageFormatException("Length limit exceeded: " + _length); } else if (_length < 0) { throw new MessageFormatException("Invalid length: " + _length); - } else if (_bufferLen < _length) { + } else if (_dataStart + _length > _bufferLen) { throw new MessageFormatException("Length of " + _length + " exceeds buffer size"); } return (int) _length; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java index 79727a260..7269f2bb9 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java @@ -83,17 +83,22 @@ public synchronized boolean hasReply() { */ public synchronized Message getReply(long _timeout) { logger.trace("Blocking on {}", this); - if (null != reply) { + if (reply != null) { return reply; } - long remainingNanos = TimeUnit.MILLISECONDS.toNanos(_timeout); - long deadline = System.nanoTime() + remainingNanos; - try { - while (null == reply && remainingNanos > 0) { - TimeUnit.NANOSECONDS.timedWait(this, remainingNanos); - remainingNanos = deadline - System.nanoTime(); + if (_timeout <= 0) { // 0/negative means wait indefinitely (like the previous wait(0)) + while (reply == null) { + wait(); + } + } else { + long remainingNanos = TimeUnit.MILLISECONDS.toNanos(_timeout); + long deadline = System.nanoTime() + remainingNanos; + while (reply == null && remainingNanos > 0) { + TimeUnit.NANOSECONDS.timedWait(this, remainingNanos); + remainingNanos = deadline - System.nanoTime(); + } } } catch (InterruptedException _ex) { Thread.currentThread().interrupt(); // keep interrupted state diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java index b122cbc8b..8df6ad2d7 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/message/AbstractInputStreamMessageReader.java @@ -240,15 +240,14 @@ public String toString() { * @param _width the width (in bytes) of the length field * @return the validated length as an integer * @throws DBusException if the extracted length exceeds the maximum allowed length - * or if the length is less than or equal to 0 + * or if the length is negative */ private int demarshallLength(byte[] _buf, int _ofs, byte _endian, int _width) throws DBusException { - // Message.demarshallint(tbuf, 0, endian, 4) long length = Message.demarshallint(_buf, _ofs, _endian, _width); if (length > Message.MAXIMUM_MESSAGE_LENGTH) { throw new DBusException("Message length exceeds maximum allowed length"); - } else if (length <= 0) { - throw new DBusException("Message length must be greater than 0"); + } else if (length < 0) { // 0 is valid, e.g. messages without a body + throw new DBusException("Message length must not be negative"); } return (int) length; } From a619a176b3481ba8122f1633c507357600c05b41 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 5 Jul 2026 19:51:05 +0200 Subject: [PATCH 05/38] More regression fixes --- .../java/org/freedesktop/dbus/connections/SASL.java | 12 +++++------- .../connections/base/AbstractConnectionBase.java | 7 ++++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index 7fe560faa..243f01f0c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -610,15 +610,13 @@ public boolean auth(SocketChannel _sock, AbstractTransport _transport) throws IO switch (state) { case INITIAL_STATE: try { - int kuid = -1; if (_transport instanceof AbstractUnixTransport aut) { - kuid = aut.getUid(_sock); - } - if (kuid >= 0) { + int kuid = aut.getUid(_sock); + if (kuid < 0) { // unix transport but peer UID could not be determined -> reject (no fail-open) + state = SaslAuthState.FAILED; + break; + } kernelUid = stupidlyEncode("" + kuid); - } else { - state = SaslAuthState.FAILED; - break; } state = SaslAuthState.WAIT_AUTH; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index 1da48777c..b9e264193 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -72,7 +72,7 @@ public abstract sealed class AbstractConnectionBase implements Closeable permits private final MessageFactory messageFactory; private final ConnectionConfig connectionConfig; - private AbstractTransport transport; + private volatile AbstractTransport transport; private volatile boolean disconnecting; @@ -360,8 +360,9 @@ public BusAddress getAddress() { * * @return true if connected */ - public synchronized boolean isConnected() { - return transport != null && transport.isConnected(); + public boolean isConnected() { + AbstractTransport t = transport; // read volatile field once to avoid a check-then-act race + return t != null && t.isConnected(); } /** From 3922943c2e324bc2d64f2f86d3340b653f2cb1f6 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 10 Jul 2026 23:04:08 +0200 Subject: [PATCH 06/38] Fixed issues when marshalling multi tuple return values --- .../org/freedesktop/dbus/Marshalling.java | 7 ++++++- .../dbus/test/MarshallingTest.java | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java index 14cf8eea4..c0e55f58a 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java @@ -771,7 +771,12 @@ public static Object[] deSerializeParameters(Object[] _parameters, Type[] _types return new Object[] {o}; } else if (!_methodCall && Struct.class.isAssignableFrom(clz)) { LOGGER.trace("(4) Deserializing Struct return"); - return deSerializeParameters(_parameters, types, _conn, true); + // Either a single struct value is returned (parameters.length == 1, the value itself is the + // struct) or several top-level return values make up the struct + Object struct = parameters.length == 1 + ? deSerializeParameter(parameters[0], clz, _conn) + : deSerializeParameter(parameters, clz, _conn); + return new Object[] {struct}; } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MarshallingTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MarshallingTest.java index e992e413c..e47d33138 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MarshallingTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MarshallingTest.java @@ -11,6 +11,7 @@ import org.freedesktop.dbus.messages.Message; import org.freedesktop.dbus.messages.MessageFactory; import org.freedesktop.dbus.messages.constants.MessageTypes; +import org.freedesktop.dbus.test.helper.structs.IntStruct; import org.freedesktop.dbus.test.helper.structs.MarkTuple; import org.freedesktop.dbus.test.helper.structs.SampleStruct; import org.freedesktop.dbus.test.helper.structs.SampleTuple; @@ -130,6 +131,25 @@ void testDeserializeParametersWithTuple() throws Exception { assertEquals("marked slot rootfs.1 as good", mt.getMessage(), "Message does not match after deSerialization"); } + @Test + void testDeserializeParametersWithStruct() throws Exception { + Type[] ts = new Type[] {IntStruct.class}; + + // case B: several top-level return values packed into one Struct (code generated with --disable-tuples) + Object[] multiValueParams = Marshalling.deSerializeParameters(new Object[] {5, 7}, ts, null); + assertTrue(multiValueParams[0] instanceof IntStruct, "Case B: expected an IntStruct"); + IntStruct multiValueStruct = (IntStruct) multiValueParams[0]; + assertEquals(5, multiValueStruct.getValue1(), "Case B: value1 does not match after deSerialization"); + assertEquals(7, multiValueStruct.getValue2(), "Case B: value2 does not match after deSerialization"); + + // case A: a single struct value returned (e.g. a method returning '(ii)') + Object[] singleValueParams = Marshalling.deSerializeParameters(new Object[] {new Object[] {5, 7}}, ts, null); + assertTrue(singleValueParams[0] instanceof IntStruct, "Case A: expected an IntStruct"); + IntStruct singleValueStruct = (IntStruct) singleValueParams[0]; + assertEquals(5, singleValueStruct.getValue1(), "Case A: value1 does not match after deSerialization"); + assertEquals(7, singleValueStruct.getValue2(), "Case A: value2 does not match after deSerialization"); + } + @Test void testDeserializeParametersVariant() throws Exception { Variant> varList = new Variant<>(List.of(1, 2, 3), "ai"); From 07ff986f26e48d97d37251c55494762f448e10ea Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 10 Jul 2026 23:24:19 +0200 Subject: [PATCH 07/38] Fixed possible issues with array length and integer casting when reading messages --- .../freedesktop/dbus/messages/Message.java | 14 +++++-- .../dbus/messages/MessageTest.java | 39 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java index e5f5793ec..e5808a8b7 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java @@ -190,6 +190,10 @@ void populate(byte[] _msg, byte[] _headers, byte[] _body, List _ for (Object o : list) { Object[] objArr = (Object[]) o; byte idx = (byte) objArr[0]; + if (idx < 0 || idx >= headers.length) { + // ignore unknown/invalid header fields + continue; + } this.headers[idx] = objArr[1]; } } @@ -1036,10 +1040,14 @@ private Object extractArray(byte[] _signatureBuf, byte[] _dataBuf, int[] _offset _offsets[OFFSET_DATA] += 4; byte algn = (byte) getAlignment(_signatureBuf[++_offsets[OFFSET_SIG]]); _offsets[OFFSET_DATA] = align(_offsets[OFFSET_DATA], _signatureBuf[_offsets[OFFSET_SIG]]); - int length = (int) (size / algn); - if (length > AbstractConnection.MAX_ARRAY_LENGTH) { - throw new MarshallingException("Arrays must not exceed " + AbstractConnection.MAX_ARRAY_LENGTH); + // validate the raw byte size (unsigned) before casting to int to avoid overflow to a negative length + if (size > AbstractConnection.MAX_ARRAY_LENGTH) { + throw new MarshallingException("Arrays must not exceed " + AbstractConnection.MAX_ARRAY_LENGTH + " bytes"); } + if (_offsets[OFFSET_DATA] + size > _dataBuf.length) { + throw new MarshallingException("Array length " + size + " exceeds remaining buffer size"); + } + int length = (int) (size / algn); rv = optimizePrimitives(_signatureBuf, _dataBuf, _offsets, size, algn, length, _options, _extractMethod); diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java index 3884573f3..69053a06a 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java @@ -1,5 +1,6 @@ package org.freedesktop.dbus.messages; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.messages.Message.ConstructorArgType; import org.freedesktop.dbus.test.AbstractBaseTest; import org.freedesktop.dbus.types.DBusListType; @@ -58,6 +59,44 @@ public void testReadMessageHeader() throws Exception { } + @Test + void testPopulateIgnoresUnknownHeaderField() { + byte[] msg = {108, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0}; + + // valid header field array, but the first field code (index 8) + // is changed from 6 (DESTINATION) to 10 -> an unknown/out-of-range header field code. + // Per the D-Bus specification unknown header fields must be ignored + byte[] headers = { + 61, 0, 0, 0, 0, 0, 0, 0, 10, 1, 115, 0, 5, 0, 0, 0, 58, 49, 46, 50, 48, 0, 0, 0, 5, 1, + 117, 0, 1, 0, 0, 0, 8, 1, 103, 0, 1, 115, 0, 0, 7, 1, 115, 0, 20, 0, 0, 0, 111, 114, + 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 68, 66, 117, 115, + 0, 0, 0, 0 + }; + byte[] body = {}; + + assertDoesNotThrow(() -> new Message().populate(msg, headers, body, null)); + } + + @Test + void testExtractArrayRejectsOversizedLength() { + byte[] msg = {108, 1, 0, 1, 4, 0, 0, 0, 1, 0, 0, 0}; + + // header field array, but the SIGNATURE field value (index 36-39) + // is changed from "s" to "ay" -> the body is expected to be a byte array + byte[] headers = { + 61, 0, 0, 0, 0, 0, 0, 0, 6, 1, 115, 0, 5, 0, 0, 0, 58, 49, 46, 50, 48, 0, 0, 0, 5, 1, + 117, 0, 1, 0, 0, 0, 8, 1, 103, 0, 2, 97, 121, 0, 7, 1, 115, 0, 20, 0, 0, 0, 111, 114, + 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 68, 66, 117, 115, + 0, 0, 0, 0 + }; + // body = a byte-array length field claiming ~4 GiB (0xFFFFFFFF); ensure this is handled properly + byte[] body = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}; + + Message m = new Message(); + assertDoesNotThrow(() -> m.populate(msg, headers, body, null)); + assertThrows(DBusException.class, m::getParameters); + } + static Stream parameterSource() { return Stream.of( new ParameterData("Complex constructor", List.of(new Type[] {long.class, String.class, byte[].class, String.class, Map.class}, new Type[] {String.class}), From 210417a388215fdac949a7a79d4727fc06c365d7 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 12 Jul 2026 17:21:37 +0200 Subject: [PATCH 08/38] Fixed interface code generator; Improved testing of generated code --- .../src/main/java/module-info.java | 1 + .../generator/InterfaceCodeGenerator.java | 16 +-- .../utils/generator/type/AnnotationInfo.java | 6 +- .../generator/type/ClassBuilderInfo.java | 6 +- .../utils/generator/type/SetterMethod.java | 23 +++- .../generator/GeneratedCodeCompiler.java | 122 ++++++++++++++++++ .../generator/InterfaceCodeGeneratorTest.java | 41 +++++- 7 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java diff --git a/dbus-java-utils/src/main/java/module-info.java b/dbus-java-utils/src/main/java/module-info.java index 20289c62e..ea792f1aa 100644 --- a/dbus-java-utils/src/main/java/module-info.java +++ b/dbus-java-utils/src/main/java/module-info.java @@ -8,6 +8,7 @@ requires java.xml; requires java.desktop; + requires java.compiler; opens org.freedesktop.dbus.utils.generator; opens org.freedesktop.dbus.utils.generator.type; diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index b8803db34..cf28e2a00 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -411,13 +411,13 @@ private List extractProperties(Element _propertyElement, Class String attrAccess = _propertyElement.getAttribute("access"); String attrType = _propertyElement.getAttribute("type"); - String access; + DBusProperty.Access access; if (DBusProperty.Access.READ.getAccessName().equals(attrAccess)) { - access = DBusProperty.Access.READ.name(); + access = DBusProperty.Access.READ; } else if (DBusProperty.Access.WRITE.getAccessName().equals(attrAccess)) { - access = DBusProperty.Access.WRITE.name(); + access = DBusProperty.Access.WRITE; } else { - access = DBusProperty.Access.READ_WRITE.name(); + access = DBusProperty.Access.READ_WRITE; } _clzBldr.getImports().add(DBusProperty.Access.class.getCanonicalName()); @@ -487,9 +487,7 @@ private List extractProperties(Element _propertyElement, Class if (DBusProperty.Access.WRITE.getAccessName().equals(attrAccess) || DBusProperty.Access.READ_WRITE.getAccessName().equals(attrAccess)) { - ClassMethod classMethod = new SetterMethod(_clzBldr, 0, attrName, rtnType); - classMethod.getArguments().add(new MemberOrArgument(_clzBldr, attrName.substring(0, 1).toLowerCase() - + attrName.substring(1), clzzName)); + ClassMethod classMethod = new SetterMethod(_clzBldr, 0, attrName, rtnType, true); _clzBldr.getMethods().add(classMethod); classMethod.getAnnotations().add(new AnnotationInfo(DBusBoundProperty.class, null)); @@ -499,8 +497,8 @@ private List extractProperties(Element _propertyElement, Class } else { AnnotArgs annotArgs = AnnotArgs.create() .add("name", attrName) - .add("type", clzzName) - .add("access", DBusProperty.Access.class.getSimpleName() + "." + access); + .add("type", AnnotClass.of(clzzName)) + .add("access", access); AnnotationInfo annotationInfo = new AnnotationInfo(DBusProperty.class, annotArgs); _clzBldr.getAnnotations().add(annotationInfo); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/AnnotationInfo.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/AnnotationInfo.java index c64303d0c..41dd36413 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/AnnotationInfo.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/AnnotationInfo.java @@ -26,7 +26,8 @@ public AnnotationInfo(Class _annotationClass, AnnotArgs _a if (_annotationParams != null) { _annotationParams.args.forEach(e -> { annotationParams.put(e.key(), e.value()); - if (e.value() != null && !e.value().getClass().getPackage().getName().startsWith("java.lang")) { + if (e.value() != null && !(e.value() instanceof AnnotClass) && !(e.value() instanceof Enum) + && !e.value().getClass().getPackage().getName().startsWith("java.lang")) { additionalImports.add(e.value().getClass()); } }); @@ -78,6 +79,9 @@ private String handleArg(Object _value) { if (_value instanceof AnnotClass ct) { return ct.fqcn() + ".class"; } + if (_value instanceof Enum en) { + return en.getDeclaringClass().getSimpleName() + "." + en.name(); + } if (_value instanceof String s && !s.endsWith(".class")) { return "\"" + s + "\""; } else { diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java index 8fc25ab30..d4a8e93ac 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/ClassBuilderInfo.java @@ -203,11 +203,7 @@ private List createClassFileContent(boolean _staticClass, Set _o allImports.add(annotation.getAnnotationClass().getName()); allImports.addAll(annotation.getAdditionalImports().stream().map(Class::getName).toList()); - String annotationCode = classIndent + "@" + annotation.getAnnotationClass().getSimpleName(); - if (annotation.getAnnotationParams() != null) { - annotationCode += "(" + annotation.getAnnotationString() + ")"; - } - content.add(annotationCode); + content.add(classIndent + annotation.getAnnotationString()); } String bgn = classIndent + "public " + (_staticClass ? "static " : "") + (getClassType() == ClassType.INTERFACE ? "interface" : "class"); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/SetterMethod.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/SetterMethod.java index a0f576386..e3f8f460c 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/SetterMethod.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/type/SetterMethod.java @@ -9,22 +9,33 @@ public class SetterMethod extends ClassMethod { %s%s } """; - private final int indentLevel; + private final int indentLevel; + private final boolean declarationOnly; + /** Setter for a concrete class member (generates a method body {@code this.x = x;}). */ public SetterMethod(ClassBuilderInfo _bldr, int _indentLevel, String _name, String _setterType) { + this(_bldr, _indentLevel, _name, _setterType, false); + } + + /** + * @param _declarationOnly if {@code true} only a declaration (no body) is generated, e.g. for interface + * property setters; the parameter name is then lower-cased for readability + */ + public SetterMethod(ClassBuilderInfo _bldr, int _indentLevel, String _name, String _setterType, boolean _declarationOnly) { super(_bldr, _name, "void", "set", false); indentLevel = _indentLevel; - getArguments().add(new MemberOrArgument(_bldr, _name, _setterType)); + declarationOnly = _declarationOnly; + String argName = _declarationOnly ? _name.substring(0, 1).toLowerCase() + _name.substring(1) : _name; + getArguments().add(new MemberOrArgument(_bldr, argName, _setterType)); } @Override protected List formatMethod(int _indentLvl, String _modifier, String _returnType, String _methodName, String _args) { - int indent = Math.max(indentLevel, _indentLvl); - - if (getArguments() == null || getArguments().isEmpty()) { - return super.formatMethod(indent, _modifier, _returnType, _methodName, _args); + if (declarationOnly || getArguments() == null || getArguments().isEmpty()) { + return super.formatMethod(_indentLvl, _modifier, _returnType, _methodName, _args); } + int indent = Math.max(indentLevel, _indentLvl); String content = String.format("this.%s = %s;", getArguments().getFirst().getName(), getArguments().getFirst().getName()); return SETTER_METHOD_TEMPL.formatted("public ", _returnType, _methodName, _args, diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java new file mode 100644 index 000000000..2092fcb2f --- /dev/null +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java @@ -0,0 +1,122 @@ +package org.freedesktop.dbus.utils.generator; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +/** + * The {@code GeneratedCodeCompiler} class is a utility for compiling generated Java source code in memory. + * It verifies that generated code is valid and free of compilation errors in the test environment, ensuring + * compatibility with required dependencies and correct formatting. Neither the sources nor the compiled + * classes are written to disk. + */ +public final class GeneratedCodeCompiler { + + private GeneratedCodeCompiler() { + + } + + /** + * Compiles all generated sources together fully in memory against the current test classpath (which contains + * the dbus-java-core annotations/types). Fails with the collected compiler errors if the generated code does + * not compile. This catches import/formatting/generics regressions that the plain string assertions cannot. + */ + static void assertCompiles(String _desc, Map _generated) throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "A JDK (with javac) is required to run this test"); + assertFalse(_generated.isEmpty(), _desc + " - no sources were generated"); + + List sources = new ArrayList<>(); + for (Entry e : _generated.entrySet()) { + String rel = e.getKey().getPath().replace(File.separatorChar, '/'); + String fqcn = rel.substring(0, rel.length() - JavaFileObject.Kind.SOURCE.extension.length()).replace('/', '.'); + sources.add(new InMemorySource(fqcn, e.getValue())); + } + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + // dependencies (dbus-java-core) may be on the module path in a modular surefire run, so combine both + String classpath = Stream.of(System.getProperty("java.class.path"), System.getProperty("jdk.module.path")) + .filter(p -> p != null && !p.isBlank()) + .collect(Collectors.joining(File.pathSeparator)); + List options = List.of("-classpath", classpath, "-proc:none"); + + try (StandardJavaFileManager stdManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); + InMemoryFileManager fileManager = new InMemoryFileManager(stdManager)) { + + boolean ok = compiler.getTask(null, fileManager, diagnostics, options, null, sources).call(); + + String errors = diagnostics.getDiagnostics().stream() + .filter(d -> d.getKind() == Diagnostic.Kind.ERROR) + .map(d -> (d.getSource() == null ? "" : d.getSource().getName() + ":" + d.getLineNumber() + ": ") + d.getMessage(null)) + .collect(Collectors.joining("\n")); + + assertTrue(ok, _desc + " - generated code did not compile:\n" + errors); + } + } + + /** In-memory Java source for the compiler. */ + private static final class InMemorySource extends SimpleJavaFileObject { + private final String code; + + InMemorySource(String _fqcn, String _code) { + super(URI.create("string:///" + _fqcn.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + code = _code; + } + + @Override + public CharSequence getCharContent(boolean _ignoreEncodingErrors) { + return code; + } + } + + /** In-memory compiled class; holds the bytecode in a buffer instead of writing it to disk. */ + private static final class InMemoryClass extends SimpleJavaFileObject { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + InMemoryClass(String _className, Kind _kind) { + super(URI.create("mem:///" + _className.replace('.', '/') + _kind.extension), _kind); + } + + @Override + public OutputStream openOutputStream() { + return bytes; + } + } + + /** File manager which redirects compiled class output into {@link InMemoryClass} buffers (no disk output). */ + private static final class InMemoryFileManager extends ForwardingJavaFileManager { + + InMemoryFileManager(StandardJavaFileManager _delegate) { + super(_delegate); + } + + @Override + public JavaFileObject getJavaFileForOutput(JavaFileManager.Location _location, String _className, + JavaFileObject.Kind _kind, FileObject _sibling) { + return new InMemoryClass(_className, _kind); + } + } +} diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java index 0605a459c..1002f8899 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java @@ -3,13 +3,6 @@ import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; import static org.junit.jupiter.api.Assertions.*; -import org.freedesktop.dbus.annotations.DBusInterfaceName; -import org.freedesktop.dbus.utils.Util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - import java.io.File; import java.util.Arrays; import java.util.List; @@ -17,6 +10,12 @@ import java.util.Map.Entry; import java.util.Set; import java.util.stream.Stream; +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.utils.Util; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class InterfaceCodeGeneratorTest { @@ -24,6 +23,34 @@ static InterfaceCodeGenerator loadDBusXmlFile(File _inputFile, String _objectPat return loadDBusXmlFile(false, _inputFile, _objectPath, _busName); } + @Test + void testGeneratedWirelessInterfaceCompiles() throws Exception { + InterfaceCodeGenerator gen = loadDBusXmlFile( + new File("src/test/resources/CreateInterface/networkmanager/org.freedesktop.NetworkManager.Device.Wireless.xml"), + "/", "org.freedesktop.NetworkManager.Device.Wireless"); + GeneratedCodeCompiler.assertCompiles("NetworkManager.Device.Wireless", gen.analyze(true)); + } + + @Test + void testGeneratedWritablePropertyCompiles() throws Exception { + String xml = """ + + + + + + """; + InterfaceCodeGenerator gen = new InterfaceCodeGenerator(false, xml, "/", "org.example.PropIface", null, true, null, false); + Map generated = gen.analyze(true); + String src = generated.values().iterator().next(); + + // setter must be a plain interface declaration: single argument, no body (old bug: duplicate arg + body) + assertTrue(src.contains("void setSimpleProp(String simpleProp);"), + "expected single-argument setter declaration, was:\n" + src); + assertFalse(src.contains("this."), "setter must not contain a method body, was:\n" + src); + GeneratedCodeCompiler.assertCompiles("writable property (--propertyMethods)", generated); + } + static InterfaceCodeGenerator loadDBusXmlFile(boolean _createPropertyMethods, File _inputFile, String _objectPath, String _busName) { if (!Util.isBlank(_busName)) { String introspectionData = Util.readFileToString(_inputFile); From 55f65819f700c2e54b30653bf3f7939737ff2696 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 12 Jul 2026 17:43:30 +0200 Subject: [PATCH 09/38] More interface code generator fixes --- .../generator/InterfaceCodeGenerator.java | 6 +- .../dbus/utils/generator/TypeConverter.java | 83 ++++++------------- .../generator/InterfaceCodeGeneratorTest.java | 26 ++++-- .../utils/generator/TypeConverterTest.java | 24 ++++++ 4 files changed, 69 insertions(+), 70 deletions(-) create mode 100644 dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/TypeConverterTest.java diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index cf28e2a00..aa2561f78 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -607,7 +607,7 @@ private String buildStructClass(List _dbusTypeStr, String _structName, root.setExtendClass(Struct.class.getName()); root.setClassType(ClassType.CLASS); - ClassConstructor classConstructor = new ClassConstructor(_clzBldr, 0, className); + ClassConstructor classConstructor = new ClassConstructor(root, 0, className); root.getConstructors().add(classConstructor); String structFqcn = _clzBldr.getPackageName() + "." + Util.upperCaseFirstChar(_structName); @@ -626,11 +626,11 @@ private String buildStructClass(List _dbusTypeStr, String _structName, root.getImports().addAll(addClasses); } - MemberOrArgument argument = new MemberOrArgument(_clzBldr, data.name(), structClassName, true); + MemberOrArgument argument = new MemberOrArgument(root, data.name(), structClassName, true); argument.getAnnotations().add(new AnnotationInfo(Position.class, AnnotArgs.create().add(i))); root.getMembers().add(argument); - classConstructor.getArguments().add(new MemberOrArgument(_clzBldr, data.name(), structClassName)); + classConstructor.getArguments().add(new MemberOrArgument(root, data.name(), structClassName)); } _structClasses.add(root); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/TypeConverter.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/TypeConverter.java index 532d46d72..dfe0c5cc1 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/TypeConverter.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/TypeConverter.java @@ -190,41 +190,38 @@ public static String getJavaTypeFromDBusType(String _dbusType, Set _java } /** - * Resolve java type recursively. + * Recursively builds the (possibly nested) java type string for the given {@link Type} and collects every + * encountered type name (raw container types and leaf types) into {@code _javaIncludes} so the required + * imports can be emitted. * - * @param _type Type object - * @return Map where key is parent classname (e.g. List) and value is a list of types used inside the generics - * - * @throws DBusException on error + * @param _type type to resolve + * @param _javaIncludes set collecting the encountered type names (imports) + * @return java type string, e.g. {@code java.util.Map>} */ - private static Map> getTypeAdv(Type _type) throws DBusException { - Map> result = new LinkedHashMap<>(); + private static String buildJavaType(Type _type, Set _javaIncludes) { if (_type instanceof ParameterizedType pType) { - - List generics = new ArrayList<>(); - result.put(pType.getRawType().getTypeName(), generics); - - for (Type t : pType.getActualTypeArguments()) { - if (t instanceof ParameterizedType) { - result.putAll(getTypeAdv(t)); - } else { - generics.add(t.getTypeName()); - } - } - } else { - result.put(_type.getTypeName(), new ArrayList<>()); + String raw = pType.getRawType().getTypeName(); + _javaIncludes.add(raw); + String args = Arrays.stream(pType.getActualTypeArguments()) + .map(t -> buildJavaType(t, _javaIncludes)) + .collect(Collectors.joining(", ")); + return raw + "<" + args + ">"; } - return result; + + String name = _type.getTypeName(); + _javaIncludes.add(name); + return name; } /** - * Special handling for {@link DBusMapType} and {@link DBusListType}. + * Special handling for {@link DBusMapType} and {@link DBusListType}. Produces the fully nested generic type + * string (arbitrary depth, distinct map key/value types) via {@link #buildJavaType(Type, Set)}. * * @param _dbusType DBus type string * @param _javaIncludes list where additional java imports are added to (if any) * @return class name of the parent type, maybe null if no suitable input provided * - * @throws DBusException + * @throws DBusException on DBus error */ private static String getTypeAdv(String _dbusType, Set _javaIncludes) throws DBusException { @@ -235,43 +232,11 @@ private static String getTypeAdv(String _dbusType, Set _javaIncludes) th List dataType = new ArrayList<>(); Marshalling.getJavaType(_dbusType, dataType, 1); - if (dataType.getFirst() instanceof DBusListType || dataType.getFirst() instanceof DBusMapType) { - ParameterizedType dBusListType = (ParameterizedType) dataType.getFirst(); - Type[] actualTypeArguments = dBusListType.getActualTypeArguments(); - - String retVal = dBusListType.getRawType().getTypeName(); - List internalTypes = new ArrayList<>(); - - if (actualTypeArguments.length > 0) { - Map> allAdvTypes = new LinkedHashMap<>(); - - for (Type type : actualTypeArguments) { - Map> typeAdv = getTypeAdv(type); - allAdvTypes.putAll(typeAdv); - } - - for (Entry> e : allAdvTypes.entrySet()) { - if (!e.getValue().isEmpty()) { - String actualArgTypeVal = e.getKey() + "<"; - actualArgTypeVal += String.join(", ", e.getValue()); - actualArgTypeVal += ">"; - internalTypes.add(actualArgTypeVal); - _javaIncludes.addAll(e.getValue()); - } else { - internalTypes.add(e.getKey()); - _javaIncludes.add(e.getKey()); - } - } - } - - // if key and value of map is of same type: - if (dataType.getFirst() instanceof DBusMapType && internalTypes.size() == 1) { - internalTypes.add(internalTypes.getFirst()); - } - - return retVal + "<" + String.join(", ", internalTypes) + ">"; + Type first = dataType.getFirst(); + if (first instanceof DBusListType || first instanceof DBusMapType) { + return buildJavaType(first, _javaIncludes); } - return dataType.getFirst().getTypeName(); + return first.getTypeName(); } } diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java index 1002f8899..54e388a66 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java @@ -51,6 +51,14 @@ void testGeneratedWritablePropertyCompiles() throws Exception { GeneratedCodeCompiler.assertCompiles("writable property (--propertyMethods)", generated); } + @Test + void testGeneratedDisableTuplesStructsCompile() throws Exception { + // --disable-tuples generates Struct classes for multi-value returns; these must carry their own imports + String xml = Util.readFileToString(new File("src/test/resources/CreateInterface/xdg-desktop/org.freedesktop.portal.Documents.xml")); + InterfaceCodeGenerator gen = new InterfaceCodeGenerator(false, xml, "/", "org.freedesktop.portal.Documents", null, false, null, true); + GeneratedCodeCompiler.assertCompiles("Documents (--disable-tuples)", gen.analyze(true)); + } + static InterfaceCodeGenerator loadDBusXmlFile(boolean _createPropertyMethods, File _inputFile, String _objectPath, String _busName) { if (!Util.isBlank(_busName)) { String introspectionData = Util.readFileToString(_inputFile); @@ -166,16 +174,18 @@ void testHandleStructSignals() throws Exception { .orElseThrow() .getValue(); - assertLineEquals(99, primaryFile, " private final List shortcuts;"); + assertLineEquals(100, primaryFile, " private final List shortcuts;"); + + assertLineEquals(102, primaryFile, " public ShortcutsChanged(String path, DBusPath sessionHandle, List shortcuts) throws DBusException {"); + assertLineEquals(103, primaryFile, " super(path, sessionHandle, shortcuts);"); + assertLineEquals(104, primaryFile, " this.sessionHandle = sessionHandle;"); + assertLineEquals(105, primaryFile, " this.shortcuts = shortcuts;"); - assertLineEquals(101, primaryFile, " public ShortcutsChanged(String path, DBusPath sessionHandle, List shortcuts) throws DBusException {"); - assertLineEquals(102, primaryFile, " super(path, sessionHandle, shortcuts);"); - assertLineEquals(103, primaryFile, " this.sessionHandle = sessionHandle;"); - assertLineEquals(104, primaryFile, " this.shortcuts = shortcuts;"); + assertLineEquals(109, primaryFile, " return sessionHandle;"); + assertLineEquals(112, primaryFile, " public List getShortcuts() {"); + assertLineEquals(113, primaryFile, " return shortcuts;"); - assertLineEquals(108, primaryFile, " return sessionHandle;"); - assertLineEquals(111, primaryFile, " public List getShortcuts() {"); - assertLineEquals(112, primaryFile, " return shortcuts;"); + GeneratedCodeCompiler.assertCompiles("GlobalShortcuts (struct signals)", analyze); String secondaryFile = analyze.entrySet().stream() .filter(e -> e.getKey().getName().equals("ShortcutsChangedShortcutsStruct.java")) diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/TypeConverterTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/TypeConverterTest.java new file mode 100644 index 000000000..d11700bba --- /dev/null +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/TypeConverterTest.java @@ -0,0 +1,24 @@ +package org.freedesktop.dbus.utils.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import java.util.HashSet; + +class TypeConverterTest { + + @Test + void testNestedGenericsKeepEveryLevel() throws Exception { + // aaas = List>> - every nesting level must be preserved (was collapsed to two levels) + assertEquals("java.util.List>>", + TypeConverter.getJavaTypeFromDBusType("aaas", new HashSet<>())); + } + + @Test + void testMapKeyAndValueTypesAreDistinct() throws Exception { + // a{asai} = Map, List> - key and value type must not collapse into one + assertEquals("java.util.Map, java.util.List>", + TypeConverter.getJavaTypeFromDBusType("a{asai}", new HashSet<>())); + } +} From 7bc5d2563040a233ec604492375d393ab2c0ad49 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 12 Jul 2026 17:59:04 +0200 Subject: [PATCH 10/38] Last interface code generator fixes --- .../generator/InterfaceCodeGenerator.java | 11 ++--- .../utils/generator/StructTreeBuilder.java | 11 ++++- .../generator/InterfaceCodeGeneratorTest.java | 46 +++++++++++++++++++ 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java index aa2561f78..0caae32cd 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGenerator.java @@ -4,7 +4,6 @@ import org.freedesktop.dbus.Tuple; import org.freedesktop.dbus.TypeRef; import org.freedesktop.dbus.annotations.DBusBoundProperty; -import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.Position; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -187,13 +186,13 @@ private Map extractAll(Element _ife) throws IOException, DBusExcep ClassBuilderInfo interfaceClass = new ClassBuilderInfo(argumentPrefix); interfaceClass.setClassType(ClassType.INTERFACE); interfaceClass.setPackageName(packageName); - interfaceClass.setDbusPackageName(fqcn.get(DbusInterfaceToFqcn.DBUS_INTERFACE_NAME)); - interfaceClass.setClassName(className); if (forcePackageName != null) { - interfaceClass.getAnnotations().add(new AnnotationInfo(DBusInterfaceName.class, - AnnotArgs.create().add(originalPackageName + "." + className) - )); + // generated package differs from the DBus namespace -> preserve the original interface name + interfaceClass.setDbusPackageName(interfaceName); + } else { + interfaceClass.setDbusPackageName(fqcn.get(DbusInterfaceToFqcn.DBUS_INTERFACE_NAME)); } + interfaceClass.setClassName(className); interfaceClass.setExtendClass(DBusInterface.class.getName()); List additionalClasses = new ArrayList<>(); diff --git a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/StructTreeBuilder.java b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/StructTreeBuilder.java index 7ff1e3419..ad8830e67 100644 --- a/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/StructTreeBuilder.java +++ b/dbus-java-utils/src/main/java/org/freedesktop/dbus/utils/generator/StructTreeBuilder.java @@ -67,9 +67,16 @@ public String buildStructClasses(String _dbusSig, String _structBaseFqcn, ClassB List structTree = buildTree(_dbusSig); String parentType = null; + String mapKeyType = null; if (!structTree.isEmpty() && Collection.class.isAssignableFrom(structTree.getFirst().getDataType())) { parentType = structTree.getFirst().getDataType().getName(); structTree = structTree.getFirst().getSubType(); + } else if (!structTree.isEmpty() && Map.class.isAssignableFrom(structTree.getFirst().getDataType())) { + // dict with a struct value, e.g. a{s(ii)} -> Map + List keyAndValue = structTree.getFirst().getSubType(); + parentType = structTree.getFirst().getDataType().getName(); + mapKeyType = keyAndValue.getFirst().getDataType().getName(); + structTree = keyAndValue.subList(1, keyAndValue.size()); } String rootStructName = findNextStructFqcn(_structBaseFqcn, generatedStructClassNames); @@ -88,7 +95,9 @@ public String buildStructClasses(String _dbusSig, String _structBaseFqcn, ClassB _generatedClasses.add(root); if (cnt == 0 && parentType != null) { - parentType += "<" + root.getClassName() + ">"; + parentType += mapKeyType != null + ? "<" + mapKeyType + ", " + root.getClassName() + ">" + : "<" + root.getClassName() + ">"; cnt++; } diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java index 54e388a66..0ed6586b2 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java @@ -59,6 +59,52 @@ void testGeneratedDisableTuplesStructsCompile() throws Exception { GeneratedCodeCompiler.assertCompiles("Documents (--disable-tuples)", gen.analyze(true)); } + @Test + void testForcedPackageEmitsSingleInterfaceNameAnnotation() throws Exception { + // forced package + mixed-case DBus namespace previously produced two @DBusInterfaceName annotations + // (one lower-cased) -> not repeatable -> compile error + String xml = """ + + + + + + """; + InterfaceCodeGenerator gen = new InterfaceCodeGenerator(false, xml, "/", + "org.freedesktop.NetworkManager.Device.Wireless", "com.example.custom", false, null, false); + Map generated = gen.analyze(true); + String src = generated.values().iterator().next(); + + int annotationCount = src.split("@DBusInterfaceName\\(", -1).length - 1; + assertEquals(1, annotationCount, "expected exactly one @DBusInterfaceName, was:\n" + src); + assertTrue(src.contains("@DBusInterfaceName(\"org.freedesktop.NetworkManager.Device.Wireless\")"), + "expected the original (mixed-case) interface name, was:\n" + src); + assertTrue(src.contains("package com.example.custom;"), "expected forced package, was:\n" + src); + GeneratedCodeCompiler.assertCompiles("forced package + mixed-case namespace", generated); + } + + @Test + void testDictWithStructValueBecomesMap() throws Exception { + // a{s(ii)} must generate Map, not a plain Struct class + String xml = """ + + + + + + + + """; + InterfaceCodeGenerator gen = new InterfaceCodeGenerator(false, xml, "/", "org.example.DictStruct", null, false, null, false); + Map generated = gen.analyze(true); + String iface = generated.entrySet().stream() + .filter(e -> e.getKey().getName().equals("DictStruct.java")) + .findFirst().orElseThrow().getValue(); + + assertTrue(iface.contains("Map Date: Fri, 17 Jul 2026 19:12:27 +0200 Subject: [PATCH 11/38] Added watchdog to terminate tcp-auth sessions when tcp-session is kept alive but no data is sent --- .../base/AbstractConnectionBase.java | 13 ++- .../transports/AbstractTransport.java | 32 ++++++- .../dbus/test/DisconnectCallbackTest.java | 87 +++++++++++++++++++ .../dbus/test/TcpAuthTimeoutTest.java | 54 ++++++++++++ .../test/helper/interfaces/SlowInterface.java | 10 +++ 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/TcpAuthTimeoutTest.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/interfaces/SlowInterface.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index b9e264193..7984a2521 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -15,6 +15,7 @@ import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.FatalDBusException; import org.freedesktop.dbus.exceptions.NotConnected; +import org.freedesktop.dbus.interfaces.CallbackHandler; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.DBusSigHandler; import org.freedesktop.dbus.matchrules.DBusMatchRule; @@ -227,7 +228,17 @@ protected final synchronized void internalDisconnect(IOException _connectionErro getLogger().debug("Notifying {} method call(s) to stop waiting for replies", getPendingCalls().size()); for (MethodCall mthCall : getPendingCalls().values()) { try { - mthCall.setReply(getMessageFactory().createError(mthCall, interrupt)); + Error errorReply = getMessageFactory().createError(mthCall, interrupt); + mthCall.setReply(errorReply); + // also fail any registered async callback so it learns about the disconnect (and is removed -> no leak) + CallbackHandler callback = getCallbackManager().removeCallback(mthCall); + if (callback != null) { + try { + callback.handleError(errorReply.getException()); + } catch (Exception _ex) { + getLogger().debug("Exception while running disconnect error callback", _ex); + } + } } catch (DBusException _ex) { getLogger().debug("Cannot set method reply to error", _ex); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java index c43a78896..570c412ff 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java @@ -22,6 +22,11 @@ import java.util.Optional; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -35,6 +40,13 @@ public abstract class AbstractTransport implements Closeable { private static final AtomicLong TRANSPORT_ID_GENERATOR = new AtomicLong(0); + /** Watchdog to abort a stuck SASL handshake (e.g. a silent/slow TCP peer) by closing the socket. */ + private static final ScheduledExecutorService AUTH_TIMEOUT_SCHEDULER = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "DBus-Auth-Timeout-Watchdog"); + t.setDaemon(true); + return t; + }); + private final ServiceLoader spiLoader = ServiceLoader.load(ISocketProvider.class, AbstractTransport.class.getClassLoader()); private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -253,13 +265,31 @@ private void authenticate(SocketChannel _sock) throws IOException { throw new IOException("SocketChannel instance required"); } SASL sasl = new SASL(config.getSaslConfig()); + + // guard the SASL handshake with a timeout: a silent/slow peer would otherwise block the (blocking) read + // in SASL.receive indefinitely. When it fires we close the socket, which unblocks the read. + int authTimeout = config.getTimeout(); + AtomicBoolean timedOut = new AtomicBoolean(false); + ScheduledFuture watchdog = authTimeout <= 0 ? null : AUTH_TIMEOUT_SCHEDULER.schedule(() -> { + timedOut.set(true); + try { + _sock.close(); + } catch (IOException _ex) { + logger.debug("Error closing socket on authentication timeout", _ex); + } + }, authTimeout, TimeUnit.MILLISECONDS); + try { if (!sasl.auth(_sock, this)) { throw new AuthenticationException("Failed to authenticate"); } } catch (IOException _ex) { _sock.close(); - throw _ex; + throw timedOut.get() ? new AuthenticationException("Authentication timed out after " + authTimeout + "ms") : _ex; + } finally { + if (watchdog != null) { + watchdog.cancel(false); + } } fileDescriptorSupported = sasl.isFileDescriptorSupported(); // false if server does not support file descriptors } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java new file mode 100644 index 000000000..bf26140ab --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java @@ -0,0 +1,87 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.interfaces.CallbackHandler; +import org.freedesktop.dbus.test.helper.interfaces.SlowInterface; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Verifies that a registered async callback is notified via handleError (and removed) when the connection is + * disconnected while the call is still pending. + */ +public class DisconnectCallbackTest extends AbstractDBusBaseTest { + + @Test + void testPendingCallbackNotifiedOnDisconnect() throws Exception { + CountDownLatch serverEntered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch errorCalled = new CountDownLatch(1); + + String path = getTestObjectPath() + "Slow"; + SlowObject obj = new SlowObject(path, serverEntered, release); + serverconn.exportObject(path, obj); + + SlowInterface remote = clientconn.getRemoteObject(getTestBusName(), path, SlowInterface.class, false); + + clientconn.callWithCallback(remote, "slowCall", new CallbackHandler() { + @Override + public void handle(String _r) { + // not expected in this test + } + + @Override + public void handleError(DBusExecutionException _ex) { + errorCalled.countDown(); + } + }); + + // make sure the server is inside the (blocking) call, so the reply is definitely still pending + assertTrue(serverEntered.await(10, TimeUnit.SECONDS), "server did not enter slow call"); + + try { + // disconnect while the call is pending -> the callback must be failed via handleError + clientconn.disconnect(); + assertTrue(errorCalled.await(10, TimeUnit.SECONDS), "handleError was not invoked on disconnect"); + } finally { + release.countDown(); // let the server method finish + // the server now replies to the already-gone client, so the daemon bounces a ServiceUnknown error + // back to the server; consume that expected error so the strict base tearDown does not trip over it + long deadline = System.currentTimeMillis() + 5000; + while (serverconn.getError() == null && System.currentTimeMillis() < deadline) { + TimeUnit.MILLISECONDS.sleep(25); + } + } + } + + public static class SlowObject implements SlowInterface { + private final String objectPath; + private final CountDownLatch serverEntered; + private final CountDownLatch release; + + SlowObject(String _objectPath, CountDownLatch _serverEntered, CountDownLatch _release) { + objectPath = _objectPath; + serverEntered = _serverEntered; + release = _release; + } + + @Override + public String slowCall() { + serverEntered.countDown(); + try { + release.await(30, TimeUnit.SECONDS); + } catch (InterruptedException _ex) { + Thread.currentThread().interrupt(); + } + return "done"; + } + + @Override + public String getObjectPath() { + return objectPath; + } + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/TcpAuthTimeoutTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/TcpAuthTimeoutTest.java new file mode 100644 index 000000000..f04074f49 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/TcpAuthTimeoutTest.java @@ -0,0 +1,54 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.transports.AbstractTransport; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.exceptions.AuthenticationException; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * Verifies that the SASL handshake over TCP aborts within the configured timeout when the peer accepts the + * connection but never sends any data (Slowloris-style), instead of blocking the handshake thread forever. + */ +public class TcpAuthTimeoutTest extends AbstractBaseTest { + + @Test + void testTcpAuthTimesOutOnSilentPeer() throws Exception { + // the tests module runs once per transport with the others excluded from the classpath; only run here + // when the TCP transport is actually available + Assumptions.assumeTrue(TransportBuilder.getRegisteredBusTypes().contains("TCP"), + "TCP transport not on classpath in this execution"); + + try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) { + Thread accepter = new Thread(() -> { + try (Socket accepted = server.accept()) { + // accept the connection but never send any SASL data + TimeUnit.SECONDS.sleep(30); + } catch (Exception _ex) { + // ignore (socket closed / interrupted) + } + }, "silent-sasl-server"); + accepter.setDaemon(true); + accepter.start(); + + BusAddress address = BusAddress.of("tcp:host=127.0.0.1,port=" + server.getLocalPort()); + + // With the watchdog the handshake must abort within ~timeout; without it, build() hangs forever. + // timeout=500 -> single connect attempt (no retries), so the whole thing finishes quickly. + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> + assertThrows(AuthenticationException.class, () -> { + try (AbstractTransport transport = TransportBuilder.create(address) + .configure().withTimeout(500).back().build()) { + fail("connect/authenticate must not succeed against a silent peer"); + } + })); + } + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/interfaces/SlowInterface.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/interfaces/SlowInterface.java new file mode 100644 index 000000000..0fb416cde --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/interfaces/SlowInterface.java @@ -0,0 +1,10 @@ +package org.freedesktop.dbus.test.helper.interfaces; + +import org.freedesktop.dbus.interfaces.DBusInterface; + +/** + * Test interface with a method that blocks server-side so a client call stays pending (reply expected). + */ +public interface SlowInterface extends DBusInterface { + String slowCall(); +} From 0c80fe5a07c6bffd62ef1f32147adfef570d8612 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 19:26:02 +0200 Subject: [PATCH 12/38] Added bounds for error queue --- .../base/AbstractConnectionBase.java | 47 +++++++++++++++++++ .../base/ConnectionMessageHandler.java | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index 7984a2521..6b2f02195 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -35,6 +35,7 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; /** * Class containing most parts required for a arbitrary connection.
@@ -47,6 +48,9 @@ public abstract sealed class AbstractConnectionBase implements Closeable permits private static final Map INFOMAP = new ConcurrentHashMap<>(); + /** Upper bound for {@link #pendingErrorQueue} to avoid unbounded growth when {@link #getError()} is never called. */ + private static final int MAX_PENDING_ERRORS = 1024; + private final Logger logger; private final ObjectTree objectTree; @@ -67,6 +71,7 @@ public abstract sealed class AbstractConnectionBase implements Closeable permits private final Map pendingCalls; private final Queue pendingErrorQueue; + private final AtomicInteger pendingErrorCount = new AtomicInteger(0); private final BusAddress busAddress; @@ -490,11 +495,53 @@ public String getExportedObject(DBusInterface _interface) throws DBusException { public DBusExecutionException getError() { Error poll = getPendingErrorQueue().poll(); if (poll != null) { + pendingErrorCount.decrementAndGet(); return poll.getException(); } return null; } + /** + * Adds an unhandled DBus error to the pending error queue in a bounded fashion. + *

+ * The queue is only drained by callers of {@link #getError()}. Applications which never call {@code getError()} + * would otherwise let this queue grow without limit. To avoid that, the queue is capped at + * {@link #MAX_PENDING_ERRORS} entries; on overflow the oldest entry is dropped (the most recent errors, which are + * usually the more relevant ones for diagnostics, are kept). + *

+ * + * @param _err error to enqueue + */ + protected void addPendingError(Error _err) { + if (offerBounded(getPendingErrorQueue(), pendingErrorCount, MAX_PENDING_ERRORS, _err)) { + getLogger().debug("Pending error queue exceeded {} entries; dropped oldest unhandled error", MAX_PENDING_ERRORS); + } + } + + /** + * Adds an element to a queue while keeping its size bounded, dropping the oldest element on overflow. + *

+ * Uses the supplied {@link AtomicInteger} as an O(1) size counter instead of {@link Queue#size()} which is O(n) for + * {@link ConcurrentLinkedQueue}. The counter must be maintained (decremented) by every other consumer that removes + * elements from the same queue. + *

+ * + * @param element type + * @param _queue queue to add to + * @param _count size counter associated with the queue + * @param _max maximum number of elements to retain + * @param _element element to add + * @return {@code true} if an element was dropped to stay within the bound, {@code false} otherwise + */ + static boolean offerBounded(Queue _queue, AtomicInteger _count, int _max, T _element) { + _queue.add(_element); + if (_count.incrementAndGet() > _max && _queue.poll() != null) { + _count.decrementAndGet(); + return true; + } + return false; + } + /** * Connects the underlying transport if it is not already connected. *

diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index 42edd7652..5794fcd43 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -165,7 +165,7 @@ public synchronized void run() { } } else { - getPendingErrorQueue().add(_err); + addPendingError(_err); } } From 5f7a350eab2c81cb533917088e7f3eebe61f1521 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 19:42:56 +0200 Subject: [PATCH 13/38] Fixed possible leak --- .../dbus/connections/base/AbstractConnectionBase.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index 6b2f02195..6d4d4f126 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -127,9 +127,8 @@ protected AbstractConnectionBase(ConnectionConfig _conCfg, TransportConfig _tran .orElseThrow(); } catch (IOException | DBusException _ex) { logger.debug("Error creating transport", _ex); - if (_ex instanceof IOException ioe) { - internalDisconnect(ioe); - } + senderService.shutdownNow(); + receivingService.shutdownNow(); throw new DBusException("Failed to connect to bus: " + _ex.getMessage(), _ex); } } From 82374ad94b2d60f7a2ececdb44af12dfbbc76216 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 19:56:51 +0200 Subject: [PATCH 14/38] Fixed blocking shutdown if shutdown was initiated from signal callback --- .../connections/base/ReceivingService.java | 26 ++++++++- .../test/DisconnectFromSignalHandlerTest.java | 53 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectFromSignalHandlerTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java index d07fe6377..0e04ce54e 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ReceivingService.java @@ -30,6 +30,9 @@ public class ReceivingService { private final Map executors = new ConcurrentHashMap<>(); + /** Marks the executor a worker thread is currently running a task for (used to detect reentrant shutdown). */ + private final ThreadLocal currentPool = new ThreadLocal<>(); + private final IThreadPoolRetryHandler retryHandler; /** @@ -127,6 +130,18 @@ int execOrFail(ExecutorNames _executor, Runnable _r) { return -1; } + // wrap the runnable so the worker thread knows which pool it belongs to while running; this lets shutdown() + // detect a reentrant call (e.g. disconnect() invoked from within a signal handler) and skip awaiting its + // own still-running worker instead of blocking until the timeout expires + Runnable task = () -> { + currentPool.set(_executor); + try { + _r.run(); + } finally { + currentPool.remove(); + } + }; + int failCount = 0; while (failCount < MAX_RETRIES) { try { @@ -136,7 +151,7 @@ int execOrFail(ExecutorNames _executor, Runnable _r) { } else if (closed || exec.isShutdown() || exec.isTerminated()) { throw new IllegalThreadPoolStateException("Receiving service already closed"); } - exec.execute(_r); + exec.execute(task); break; // execution done, no retry needed } catch (IllegalThreadPoolStateException _ex) { // just throw our exception throw _ex; @@ -178,12 +193,21 @@ ExecutorService getExecutor(ExecutorNames _executor) { * @param _unit time unit */ public synchronized void shutdown(int _timeout, TimeUnit _unit) { + // when shutdown is triggered from within one of our own worker threads (e.g. disconnect() called from a + // signal handler), that thread cannot terminate itself; awaiting its pool would block until the timeout + // expires. Skip awaiting that pool - it is force-stopped by the subsequent shutdownNow(). + ExecutorNames selfPool = currentPool.get(); + for (Entry es : executors.entrySet()) { logger.debug("Shutting down executor: {}", es.getKey()); es.getValue().shutdown(); } for (Entry es : executors.entrySet()) { + if (es.getKey() == selfPool) { + logger.debug("Skipping awaitTermination for {}: shutdown triggered from within that pool", selfPool); + continue; + } try { es.getValue().awaitTermination(_timeout, _unit); } catch (InterruptedException _ex) { diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectFromSignalHandlerTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectFromSignalHandlerTest.java new file mode 100644 index 000000000..5298c9d4f --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectFromSignalHandlerTest.java @@ -0,0 +1,53 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.messages.DBusSignal; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Verifies that calling {@link org.freedesktop.dbus.connections.impl.DBusConnection#disconnect()} from within a signal + * handler does not stall for the full receiving-service shutdown timeout (~10s). The handler runs on the connection's + * SIGNAL executor; disconnect() must not block awaiting termination of that very pool. + */ +public class DisconnectFromSignalHandlerTest extends AbstractDBusBaseTest { + + @Test + void testDisconnectFromSignalHandlerDoesNotStall() throws Exception { + CountDownLatch handlerDone = new CountDownLatch(1); + AtomicLong disconnectMillis = new AtomicLong(-1); + + clientconn.addSigHandler(StallSignalService.StallSignal.class, s -> { + long start = System.nanoTime(); + clientconn.disconnect(); // called from within the SIGNAL executor thread + disconnectMillis.set((System.nanoTime() - start) / 1_000_000); + handlerDone.countDown(); + }); + + // trigger the signal from the server side + serverconn.sendMessage(new StallSignalService.StallSignal(getTestObjectPath())); + + // with the fix the handler returns quickly; without it disconnect() blocks ~10s awaiting its own SIGNAL pool + assertTimeoutPreemptively(Duration.ofSeconds(6), () -> + assertTrue(handlerDone.await(6, TimeUnit.SECONDS), "signal handler did not finish disconnect() in time")); + + assertTrue(disconnectMillis.get() >= 0, "disconnect() was not measured"); + assertTrue(disconnectMillis.get() < 5000, + "disconnect() from signal handler stalled: " + disconnectMillis.get() + " ms"); + } + + @DBusInterfaceName("org.freedesktop.dbus.test.StallSignalService") + public interface StallSignalService extends DBusInterface { + class StallSignal extends DBusSignal { + public StallSignal(String _path) throws DBusException { + super(_path); + } + } + } +} From 3b755974cf5263e2842cc5fa447b1c48e6c764c6 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 20:08:45 +0200 Subject: [PATCH 15/38] Fixed possible ArrayIndexOutOfBoundsException when reading dbus cookie; Fixed possible issue comparing hashes --- .../freedesktop/dbus/connections/SASL.java | 47 +++++++++++++++---- .../dbus/connections/SASLTest.java | 26 ++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index 243f01f0c..2a3cf5925 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.StandardOpenOption; @@ -124,6 +125,37 @@ private String findCookie(String _context, String _id) throws IOException { } } + /** Classification of a line read from the DBus cookie file. */ + enum CookieLineState { + /** Well-formed and not yet expired - keep it. */ + KEEP, + /** Well-formed but older than {@link #COOKIE_TIMEOUT} - drop silently. */ + EXPIRED, + /** Not parseable (missing timestamp field or non-numeric timestamp) - drop and warn. */ + MALFORMED + } + + /** + * Classifies a single cookie file line. A line consists of {@code }; a line without a + * timestamp field (no space) or with a non-numeric timestamp is treated as malformed instead of throwing. + * + * @param _line raw line from the cookie file + * @param _timestamp current timestamp used to detect expired cookies + * @return classification of the line + */ + static CookieLineState classifyCookieLine(String _line, long _timestamp) { + String[] parts = _line.split(" "); + if (parts.length < 2) { + return CookieLineState.MALFORMED; + } + try { + long time = Long.parseLong(parts[1]); + return (_timestamp - time) < COOKIE_TIMEOUT ? CookieLineState.KEEP : CookieLineState.EXPIRED; + } catch (NumberFormatException _ex) { + return CookieLineState.MALFORMED; + } + } + @SuppressWarnings("checkstyle:emptyblock") private void addCookie(String _context, String _id, long _timestamp, String _cookie) throws IOException { File keyringDir = DBUS_KEYRINGS_DIR; @@ -167,15 +199,10 @@ private void addCookie(String _context, String _id, long _timestamp, String _coo try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(cookiefile)))) { String s = null; while (null != (s = r.readLine())) { - String[] line = s.split(" "); - try { - long time = Long.parseLong(line[1]); - // expire stale cookies - if ((_timestamp - time) < COOKIE_TIMEOUT) { - lines.add(s); - } - } catch (NumberFormatException _ex) { - logger.warn("Ignoring malformed cookie line {}", s); + switch (classifyCookieLine(s, _timestamp)) { + case KEEP -> lines.add(s); + case MALFORMED -> logger.warn("Ignoring malformed cookie line {}", s); + case EXPIRED -> { } // silently drop stale cookie } } } @@ -416,7 +443,7 @@ SaslResult doResponse(int _auth, String _uid, String _kernelUid, SASL.Command _c byte[] buf = md.digest(prehash.getBytes()); String posthash = stupidlyEncode(buf); logger.debug("Authenticating Hash; data={} remote-hash={} local-hash={}", prehash, hash, posthash); - if (0 == COL.compare(posthash, hash)) { + if (MessageDigest.isEqual(posthash.getBytes(StandardCharsets.US_ASCII), hash.getBytes(StandardCharsets.US_ASCII))) { return SaslResult.OK; } else { return SaslResult.ERROR; diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/SASLTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/SASLTest.java index b59739479..01af3ab23 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/SASLTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/SASLTest.java @@ -2,6 +2,7 @@ import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; import org.freedesktop.dbus.connections.SASL.Command; +import org.freedesktop.dbus.connections.SASL.CookieLineState; import org.freedesktop.dbus.connections.SASL.SaslCommand; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; @@ -36,6 +37,31 @@ void testCommandAuth() throws IOException { assertNull(cmdData.getData()); } + @Test + void testClassifyCookieLineKeepsFreshEntry() { + // diff (now - time) < COOKIE_TIMEOUT (240) -> keep + assertEquals(CookieLineState.KEEP, SASL.classifyCookieLine("1 1000 abcdef", 1100)); + assertEquals(CookieLineState.KEEP, SASL.classifyCookieLine("1 1000 abcdef", 1000)); + } + + @Test + void testClassifyCookieLineDropsExpiredEntry() { + // diff (now - time) >= COOKIE_TIMEOUT (240) -> expired + assertEquals(CookieLineState.EXPIRED, SASL.classifyCookieLine("1 1000 abcdef", 2000)); + } + + @Test + void testClassifyCookieLineWithoutSpaceIsMalformed() { + // a line without a space previously caused an ArrayIndexOutOfBoundsException on line[1] + assertEquals(CookieLineState.MALFORMED, SASL.classifyCookieLine("noSpaceHere", 1000)); + assertEquals(CookieLineState.MALFORMED, SASL.classifyCookieLine("", 1000)); + } + + @Test + void testClassifyCookieLineWithNonNumericTimestampIsMalformed() { + assertEquals(CookieLineState.MALFORMED, SASL.classifyCookieLine("1 notanumber abcdef", 1000)); + } + @Test void testAnonymousAuthentication() throws DBusException { String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); From 789d8cc0b92e4a9d101580ff8a5b7cab05567dde Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 20:30:36 +0200 Subject: [PATCH 16/38] updated readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 8d57c5abd..6752e2787 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,17 @@ The library will remain open source and MIT licensed and can still be used, fork - Fixed SASL authentication issue when running in server mode in combination with unix sockets ([#298](https://github.com/hypfvieh/dbus-java/issues/298)) - Fixed various issues with `InterfaceCodeGenerator` ([#302](https://github.com/hypfvieh/dbus-java/issues/302), [#303](https://github.com/hypfvieh/dbus-java/issues/303), [#304], (https://github.com/hypfvieh/dbus-java/issues/304), [#306](https://github.com/hypfvieh/dbus-java/issues/306) - Refactoring and overhaul of `InterfaceCodeGenerator` to improve code, reduce duplications and allow easier fixing/extending + - Hardened message parsing against malformed or malicious wire data: unknown header field codes are now ignored instead of throwing, oversized arrays are rejected before the length is truncated by an `int` cast, and the data offset is included when validating string/signature lengths (previously an `ArrayIndexOutOfBoundsException`/`StringIndexOutOfBoundsException` could tear down the connection) + - Enforce an actual recursion-depth limit when converting wire types to Java types in `Marshalling` (the previous check accidentally used the type count instead of the nesting depth) + - Remove the empty match-rule queue when `AddMatch` fails, so a later `addSigHandler` re-sends `AddMatch` instead of silently never subscribing + - Disconnect the connection on an unexpected `RuntimeException` in the incoming-message thread instead of spinning in a busy-loop / flooding the log (`DBusException` still logs and continues) + - Notify and clean up pending async callbacks (`CallbackHandler`) on disconnect instead of leaking them + - Added a read-timeout watchdog for the SASL handshake to prevent connections from hanging indefinitely (e.g. Slowloris-style stalls over TCP) + - Fixed `disconnect()` stalling for the whole receiving-service shutdown timeout when called from within a signal handler + - Use constant-time comparison for `DBUS_COOKIE_SHA1` hash verification + - Fixed possible `ArrayIndexOutOfBoundsException` when parsing malformed cookie lines during SASL auth + - Bounded the pending-error queue to avoid unbounded memory growth for unhandled errors + - Clean up sender/receiver executor services when the connection fails to establish ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) From 2cf1aafd68da3abc34a51a04c2d210a3d8fc848d Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 20:33:08 +0200 Subject: [PATCH 17/38] updated readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6752e2787..b992d3ef6 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ The library will remain open source and MIT licensed and can still be used, fork - Fixed possible `ArrayIndexOutOfBoundsException` when parsing malformed cookie lines during SASL auth - Bounded the pending-error queue to avoid unbounded memory growth for unhandled errors - Clean up sender/receiver executor services when the connection fails to establish + - Added support for interactive authorization ([#PR313](https://github.com/hypfvieh/dbus-java/issues/313)), thanks to ([unfamiliarS](https://github.com/unfamiliarS) ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) From 3cd87420c507a7fe54a47b77ca622ca215c05140 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 21:03:28 +0200 Subject: [PATCH 18/38] Updated documention --- src/site/markdown/code-generation.md | 22 ++++++++++ src/site/markdown/dbus-types.md | 27 ++++++------ src/site/markdown/exporting-objects.md | 4 +- src/site/markdown/howto.md | 19 ++++++++- src/site/markdown/index.md | 57 ++++++++++++++++++++++++++ src/site/markdown/properties.md | 6 ++- src/site/markdown/quick-start.md.vm | 24 ++++++++--- src/site/markdown/remote-objects.md | 8 ++-- src/site/markdown/using-signals.md | 16 ++++---- src/site/markdown/variant-handling.md | 20 +++++++-- src/site/site.xml | 1 + 11 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 src/site/markdown/index.md diff --git a/src/site/markdown/code-generation.md b/src/site/markdown/code-generation.md index 52e1b9577..77310e858 100644 --- a/src/site/markdown/code-generation.md +++ b/src/site/markdown/code-generation.md @@ -45,3 +45,25 @@ Therefore it is recommended to run the code generator using Maven or your develo -Dexec.args="--inputFile /tmp/org.freedesktop.UDisks2.xml --outputDir /tmp/classes ' '" In both cases the generated classes/interfaces will be written to the provided output directory. + +## Command line options + +The most important options of `InterfaceCodeGenerator` are listed below. Run the generator with +`--help` to see the full and authoritative list. + +|Option|Short|Description| +|------|-----|-----------| +|`--system`|`-y`|Read introspection data from the SYSTEM bus| +|`--session`|`-s`|Read introspection data from the SESSION bus| +|`--inputFile `|`-i`|Use `` (XML introspection) as input instead of querying a running bus| +|`--outputDir

`|`-o`|Write all generated files to `` (required)| +|`--package `|`-p`|Use `` as the Java package instead of deriving it from the DBus namespace| +|`--all`|`-a`|Generate all classes for the given bus name (do not filter)| +|`--propertyMethods`|`-m`|Generate getter/setter methods for properties (see [Properties](./properties.html))| +|`--disable-tuples`|`-t`|Generate `Struct` based classes for multi-value return methods instead of `Tuple` classes (**Caution:** the generated code only works with dbus-java 6.0.0+)| +|`--argumentPrefix `| |Prepend `` to generated method arguments/parameters (e.g. `_` to match the dbus-java code style)| +|`--enable-dtd-validation`| |Enable DTD validation of the introspection XML| + +When using `--inputFile`, the `busname`/`object` argument may be omitted (or `*` can be used) to +extract all interfaces found in the file. If a non-blank `busname` is given, only interfaces +starting with that name are extracted. diff --git a/src/site/markdown/dbus-types.md b/src/site/markdown/dbus-types.md index a475b9717..2e3c30bc0 100644 --- a/src/site/markdown/dbus-types.md +++ b/src/site/markdown/dbus-types.md @@ -14,34 +14,37 @@ The following table contains a mapping of DBus types to DBus-Java types |t |org.freedesktop.dbus.types.UInt64| |d |double | |s |String | -|o |org.freedesktop.dbus.ObjectPath| -|g |??? | +|o |org.freedesktop.dbus.DBusPath| +|g |String (a DBus type signature)| |a |java.util.List| |() struct |org.freedesktop.dbus.Struct| -|v |org.freedesktop.types.Variant| +|v |org.freedesktop.dbus.types.Variant| |{} dictionary |java.util.Map| |h |org.freedesktop.dbus.FileDescriptor*| -*File Descriptor passing is not enabled by default - you need to add the -3rd-party component [dbus-java-nativefd](https://github.com/rm5248/dbus-java-nativefd) +*File descriptor passing (`h`) requires a transport that supports it. When using the +`dbus-java-transport-junixsocket` transport (recommended, available since dbus-java 4.3.1), +file descriptors work out of the box - no additional dependency is required. +See the [README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors) for +details and for the legacy setup using the `dbus-java-transport-jnr-unixsocket` transport. ## Examples If we have the DBus signature for a method of `iid`, that means the method looks like the following: -``` -void methodname( int a, int b, double c ); +```java +void methodname(int a, int b, double c); ``` A DBus signature of `ai` would be a list of integers: -``` -void methodname( List a ); +```java +void methodname(List a); ``` -A Dbus signature of `asid` would be the following: +A DBus signature of `asid` would be the following: +```java +void methodname(List a, int b, double c); ``` -void methodname( List a, int b, double c ); -``` \ No newline at end of file diff --git a/src/site/markdown/exporting-objects.md b/src/site/markdown/exporting-objects.md index 76ea082ca..cd2639762 100644 --- a/src/site/markdown/exporting-objects.md +++ b/src/site/markdown/exporting-objects.md @@ -127,12 +127,12 @@ Next, we can go call the remote method using dbus-send and get the result of the addition back: ``` -$ dbus-send --print-reply=literal --type=method_call --dest=test.dbusjava.export / com.foo.IntInterface.add int32:5 int32:7 +$ dbus-send --print-reply=literal --type=method_call --dest=test.dbusjava.export / com.github.hypfvieh.dbus.examples.export.ISampleExport.add int32:5 int32:7 int32 12 ``` Or we can use the `terminateApp()` method to stop our application: ``` -$ dbus-send --print-reply=literal --type=method_call --dest=test.dbusjava.export / com.foo.IntInterface.terminateApp +$ dbus-send --print-reply=literal --type=method_call --dest=test.dbusjava.export / com.github.hypfvieh.dbus.examples.export.ISampleExport.terminateApp ``` \ No newline at end of file diff --git a/src/site/markdown/howto.md b/src/site/markdown/howto.md index be675f76e..26dc81732 100644 --- a/src/site/markdown/howto.md +++ b/src/site/markdown/howto.md @@ -12,6 +12,10 @@ Here are some references to example code to demonstrate how to... ### Use structs * [StructServer/StructClient](https://github.com/hypfvieh/dbus-java/tree/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/struct) + +### Export properties using getters/setters (bound properties) + * [ExportObjectWithProperties](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/properties/ExportObjectWithProperties.java) + * See also the [Properties](./properties.html) guide ### Get a remote interface * [NetworkManagerExample](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/networkmanager/NetworkManagerExample.java) @@ -26,7 +30,18 @@ Here are some references to example code to demonstrate how to... ### Use EmbeddedDBusDaemon * [RunDaemon](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/daemon/RunDaemon.java) - + +### Run daemon and client in separate processes + * [RunTwoPartDaemon](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/daemon/twopart/RunTwoPartDaemon.java) + * [RunTwoPartClient](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/daemon/twopart/RunTwoPartClient.java) + +### Call privileged methods (e.g. systemd) + * [SystemdUnitsManagment](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/systemd/SystemdUnitsManagment.java) + ### Using Variant with proper type * [NetworkManagerExample3](https://github.com/hypfvieh/dbus-java/blob/master/dbus-java-examples/src/main/java/com/github/hypfvieh/dbus/examples/networkmanager/NetworkManagerExample3.java) - * See also: [Issue 74](https://github.com/hypfvieh/dbus-java/issues/74#issuecomment-1280768515) \ No newline at end of file + * See also the [Variant](./variant-handling.html) guide (including `VariantBuilder`) + * See also: [Issue 74](https://github.com/hypfvieh/dbus-java/issues/74#issuecomment-1280768515) + +### Generate Java interfaces from introspection data + * See the [Code Generation](./code-generation.html) guide \ No newline at end of file diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md new file mode 100644 index 000000000..4a318465e --- /dev/null +++ b/src/site/markdown/index.md @@ -0,0 +1,57 @@ +# dbus-java + +dbus-java is a pure-Java implementation of the [D-Bus](https://www.freedesktop.org/wiki/Software/dbus/) +protocol. D-Bus is a message bus system used on most Linux systems for inter-process communication, +for example to talk to system services such as NetworkManager, systemd, BlueZ or UPower, or to +exchange messages between applications on the user's session bus. + +With dbus-java you can: + + * connect to the SESSION or SYSTEM bus (or a custom address), + * call methods on remote objects exported by other applications, + * export your own objects so other applications can call them, + * send and receive signals, + * read and write properties, + * run an embedded D-Bus daemon for testing. + +If you are new here, start with the **[Quickstart](./quick-start.html)**. + +## Module overview + +dbus-java is split into a small core library plus interchangeable transport modules. Pick the +core plus exactly one transport that fits your platform; the other modules are optional. + +|Module|Purpose| +|------|-------| +|`dbus-java-core`|The main library (connections, marshalling, message handling). Always required.| +|`dbus-java-transport-native-unixsocket`|Unix socket transport using the JDK's native unix domain socket support (Java 16+). No extra native dependency.| +|`dbus-java-transport-junixsocket`|Unix socket transport using [junixsocket](https://github.com/kohlschutter/junixsocket). Supports file descriptor passing out of the box.| +|`dbus-java-transport-jnr-unixsocket`|Unix socket transport using [JNR](https://github.com/jnr/jnr-unixsocket).| +|`dbus-java-transport-tcp`|TCP transport (mostly for testing or remote buses).| +|`dbus-java-utils`|Tools, most notably the `InterfaceCodeGenerator` (see [Code Generation](./code-generation.html)).| +|`dbus-java-bom`|A Maven Bill-of-Materials to keep the module versions aligned.| + +A typical dependency set is `dbus-java-core` + one transport. See the +[Quickstart](./quick-start.html) for a ready-to-copy Maven snippet, and the +[README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors) for guidance on +choosing a transport (for example when you need file descriptor support). + +## Where to go next + + * [Quickstart](./quick-start.html) - add the dependencies and open a connection + * [DBus Types](./dbus-types.html) - how D-Bus types map to Java types + * [Exporting Objects](./exporting-objects.html) - make your objects callable on the bus + * [Calling Remote Objects](./remote-objects.html) - call methods on other applications + * [Properties](./properties.html) - expose and consume D-Bus properties + * [Variant](./variant-handling.html) - work with the `Variant` wrapper type + * [Signals](./using-signals.html) - send and receive signals + * [Code Generation](./code-generation.html) - generate interfaces from introspection data + * [Howto...](./howto.html) - task-oriented links into the example code + +## Further resources + + * **Javadoc** - the API reference, linked from the project [README](https://github.com/hypfvieh/dbus-java) + and available on [javadoc.io](https://javadoc.io/doc/com.github.hypfvieh/dbus-java-core) + * **Examples** - runnable code in the + [dbus-java-examples](https://github.com/hypfvieh/dbus-java/tree/master/dbus-java-examples) module + * **Wiki** - additional articles in the [GitHub Wiki](https://github.com/hypfvieh/dbus-java/wiki) diff --git a/src/site/markdown/properties.md b/src/site/markdown/properties.md index 515dd0b09..7b52a3861 100644 --- a/src/site/markdown/properties.md +++ b/src/site/markdown/properties.md @@ -203,7 +203,7 @@ public class ImportMyObject { ## Implementing the Properties Interface -As an alternative to the above, you can use DBus's `org.freedesktop.dbus.Properties` interface. +As an alternative to the above, you can use DBus's `org.freedesktop.dbus.interfaces.Properties` interface. If you are exporting your own service, this means that you `extends Properties` in your interface, and provide the required implementations of the `Get()`, `GetAll()` and `Set()` methods in your @@ -237,6 +237,10 @@ or when you need to access these properties. ```java package com.acme; +import java.util.HashMap; +import java.util.Map; +import org.freedesktop.dbus.types.Variant; + public class MyObject implements MyInterface { private String myProperty = "Initial value"; diff --git a/src/site/markdown/quick-start.md.vm b/src/site/markdown/quick-start.md.vm index 02a3f1445..2586efad3 100644 --- a/src/site/markdown/quick-start.md.vm +++ b/src/site/markdown/quick-start.md.vm @@ -43,17 +43,18 @@ For maven this would look like this: DBus-Java uses SLF4J internally for logging. ``` + org.apache.logging.log4j log4j-api - 2.17.2 + 2.24.3 org.apache.logging.log4j - log4j-slf4j-impl - 2.17.2 + log4j-slf4j2-impl + 2.24.3 ``` @@ -100,8 +101,8 @@ received messages. There are signals, methods and method returns. To prevent one message processing blocking other message processing, each of those message types are handled in different thread pools. -The default settings is 1 thread per message type for signals, methods and errors. Method-Return messages have a thread pool -size of 4. +The default is a thread pool size of 1 for signals, errors and method-returns. Method-Call messages use a thread pool +size of 4 (a size greater than 1 is required to handle recursive/re-entrant incoming calls). To change those settings, use the `receivingThreadConfig()` method on the connection builders. Example: @@ -120,6 +121,19 @@ If you do not want to do additional configuration you can also call `.buildConne With this pattern you can also change the thread priority for each pool. +#[[##]]## Using virtual threads +Since dbus-java 6.0.0 the receiving thread pools can use virtual threads (JEP 444) instead of native threads. +Virtual threads can be enabled per executor (`SIGNAL`, `ERROR`, `METHODCALL`, `METHODRETURN`) or for all of them at once. +The default remains native threads on all executors. + +```java + DBusConnection conn = DBusConnectionBuilder.forSessionBus() + .receivingThreadConfig() + .withAllVirtualThreads(true) + .connectionConfig() + .build(); +``` + #[[##]]## IMPORTANT NOTE ABOUT SIGNAL THREADPOOL Increasing the signal thread pool will improve the speed the signals are handled. Nevertheless increasing the thread pool may also cause the signals to be handled in any order, not necessarily in the order diff --git a/src/site/markdown/remote-objects.md b/src/site/markdown/remote-objects.md index a50211ff9..96af352a5 100644 --- a/src/site/markdown/remote-objects.md +++ b/src/site/markdown/remote-objects.md @@ -22,7 +22,7 @@ public class RemoteExample { m_conn = DBusConnectionBuilder.forSessionBus().build(); /* Get the remote object */ - IntInterface i = m_conn.getRemoteObject( "test.dbusjava.export", "/", IntInterface.class ); + ISampleExport i = m_conn.getRemoteObject("test.dbusjava.export", "/", ISampleExport.class ); System.out.println( i.add( 5, 7 ) ); } @@ -54,7 +54,9 @@ When calling remote D-Bus methods, you can influence how the call is handled by @MethodNoReply int add(int _a, int _b); ``` - Returns null even for the arguments from the example above. + The call returns immediately without waiting for a reply, so the return value carries no + result: for object return types it is `null`, for primitive types it is the default value + (e.g. `0` for `int`). Use this annotation only for methods whose result you do not need. * **`@MethodAllowInteractiveAutorization`** This annotation signals the D-Bus daemon that the caller is ready to wait for interactive authorization (e.g., Polkit password prompts). It is useful when unprivileged code calls a privileged method, and an authorization framework that supports user interaction is in place. @@ -71,4 +73,4 @@ When calling remote D-Bus methods, you can influence how the call is handled by ``` After calling the method, the user authorization window is guaranteed to be displayed if it needed. -*Note:* More info about this you can find [here](https://dbus.freedesktop.org/doc/dbus-specification.html). \ No newline at end of file +*Note:* More info about this you can find [here](https://dbus.freedesktop.org/doc/dbus-specification.html). diff --git a/src/site/markdown/using-signals.md b/src/site/markdown/using-signals.md index 9eea71c3a..c6a073664 100644 --- a/src/site/markdown/using-signals.md +++ b/src/site/markdown/using-signals.md @@ -52,15 +52,15 @@ There are different `addSigHandler` methods depending on the use case. If you only want to listen for specific signals of a specific remote object, you should use something like: ```java -MySignal remoteSignal connection.getRemoteObject("some.bus.name", "/some/object/path", MySignal.class); -connection.addSigHandler(MySignalClass.class, remoteSignal, new MySignalClassHandler()); +MySignal remoteSignal = connection.getRemoteObject("some.bus.name", "/some/object/path", MySignal.class); +connection.addSigHandler(MySignal.MySignalClass.class, remoteSignal, new MySignalClassHandler()); ``` If you want to listen to all object paths of an exported object you can use: -`connection.addSigHandler(MySignalClass.class, new MySignalClassHandler())`. +`connection.addSigHandler(MySignal.MySignalClass.class, new MySignalClassHandler())`. It is also possible to express a signal handler as Lambda e.g.: -`connection.addSigHandler(MySignalClass.class, signal -> System.out.println("Got signal: " + signal))` +`connection.addSigHandler(MySignal.MySignalClass.class, signal -> System.out.println("Got signal: " + signal))` To remove a handler you can either call `close()` on the object returned by the `addSigHandler` calls or use the appropriate `removeSigHandler` call. You don't have to remove your signal handler when you want to close the connection anyway. @@ -144,11 +144,11 @@ Example: ```java class MySignal extends DBusSignal { public MySignal(String _objPath, int[] _arr, String _text) { - super(_objectPath, _arr, _text); + super(_objPath, _arr, _text); } public MySignal(String _objPath, List _arr, String _text) { - super(_objectPath, _arr, _text); + super(_objPath, _arr, _text); } } ``` @@ -162,11 +162,11 @@ The example would then look like this: ```java class MySignal extends DBusSignal { public MySignal(String _objPath, int[] _arr, String _text) { - this(_objectPath, Arrays.asList(_arr), _text); + this(_objPath, Arrays.stream(_arr).boxed().toList(), _text); } public MySignal(String _objPath, List _arr, String _text) { - super(_objectPath, _arr, _text); + super(_objPath, _arr, _text); } } ``` diff --git a/src/site/markdown/variant-handling.md b/src/site/markdown/variant-handling.md index bac909bff..3162b7c9b 100644 --- a/src/site/markdown/variant-handling.md +++ b/src/site/markdown/variant-handling.md @@ -32,9 +32,11 @@ therefore a `List` that was used in Java will become an array in DBus terms. That means, serializing `Variant>` will create the same DBus signature as serializing `Variant` or `Variant`. -When getting data from the bus to convert back to `Variant>` the information that the Variant -should contain a `List` and not an array is not present. -From DBus standpoint the data is organized as array therefore the Variant will contain an array of int (`int[]`) and not a `List`. +When getting data from the bus, the information whether the `Variant` originally held a `List` or an array is not present - +from the DBus standpoint the data is always organized as an array. Since dbus-java 5.1.0 such data is therefore **always** +deserialized as a `List` (see the section "Changes introduced with dbus-java 5.1.0" below). +So a `Variant` that was sent will be received as a `Variant>` - a `Variant` will never contain an +array after deserialization. ## How to put a Collection / Map into a `Variant` @@ -63,6 +65,18 @@ Usage with `Variant` constructor: `new Variant<>(Set.of(1, 2, 3), Marshalling.convertJavaClassesToSignature(Set.class, Integer.class))`; `new Variant<>(Map.of("foo", true, "bar", false), Marshalling.convertJavaClassesToSignature(Map.class, String.class, Boolean.class));` +### Using `VariantBuilder` + +Since dbus-java 5.1.1 there is a more convenient way to build a `Variant` around a Collection or Map without +having to compute the signature yourself: `org.freedesktop.dbus.types.VariantBuilder`. +You declare the container type and its generic types, and the builder derives the correct signature for you. + +```java +Variant> v1 = VariantBuilder.of(List.class).withGenericTypes(String.class).create(List.of("foo", "bar")); +Variant> v2 = VariantBuilder.of(Set.class).withGenericTypes(Integer.class).create(Set.of(1, 2, 3)); +Variant> v3 = VariantBuilder.of(Map.class).withGenericTypes(String.class, Boolean.class).create(Map.of("foo", true)); +``` + ## Changes introduced with dbus-java 5.1.0 Starting with dbus-java 5.1.0 the behavior of `Variant` has been changed regarding the support of Collections and arrays. diff --git a/src/site/site.xml b/src/site/site.xml index d70eef64f..c6f35d822 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -29,6 +29,7 @@ + From 1ecdbe53c855fc3761c6217c11a2c308c70664aa Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 21:28:04 +0200 Subject: [PATCH 19/38] Fixed bugs in Dbus matchrule handling --- .../dbus/matchrules/DBusMatchRuleBuilder.java | 15 ++--- .../dbus/matchrules/MatchRuleMatcher.java | 57 +++++++++++-------- .../dbus/matchrules/MatchRuleMatcherTest.java | 17 +++++- .../dbus/matchrules/MatchRuleParserTest.java | 16 ++++++ 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/DBusMatchRuleBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/DBusMatchRuleBuilder.java index 933d9c12f..5bf4298ca 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/DBusMatchRuleBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/DBusMatchRuleBuilder.java @@ -372,20 +372,15 @@ DBusMatchRule fromMap(Map _keyValues) { }); if (!copy.isEmpty()) { - Pattern argPattern = Pattern.compile("^arg([0-9]{1,2})$"); - Pattern argPathPattern = Pattern.compile("^arg([0-9]{1,2})path$"); + Pattern argPattern = Pattern.compile("^arg([0-9]+)$"); + Pattern argPathPattern = Pattern.compile("^arg([0-9]+)path$"); for (Entry e : copy.entrySet()) { Matcher argMatcher = argPattern.matcher(e.getKey()); Matcher argPathMatcher = argPathPattern.matcher(e.getKey()); if (argMatcher.matches()) { - multiValueFields - .computeIfAbsent(MatchRuleField.ARG0123, x -> new LinkedHashMap<>()) - .putIfAbsent(Integer.valueOf(argMatcher.group(1)), e.getValue()); - } - if (argPathMatcher.matches()) { - multiValueFields - .computeIfAbsent(MatchRuleField.ARG0123PATH, x -> new LinkedHashMap<>()) - .putIfAbsent(Integer.valueOf(argMatcher.group(1)), e.getValue()); + withArgX(MatchRuleField.ARG0123, Integer.parseInt(argMatcher.group(1)), e.getValue()); + } else if (argPathMatcher.matches()) { + withArgX(MatchRuleField.ARG0123PATH, Integer.parseInt(argPathMatcher.group(1)), e.getValue()); } } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/MatchRuleMatcher.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/MatchRuleMatcher.java index 867b10ac8..687820dd6 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/MatchRuleMatcher.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/matchrules/MatchRuleMatcher.java @@ -47,20 +47,23 @@ static boolean matchArg0123(Message _msg, Map _compare) { Object[] parameters = _msg.getParameters(); - for (int i = 0; i < parameters.length; i++) { - if (!_compare.containsKey(i)) { - continue; + // all configured argument constraints must match (logical AND) + for (Map.Entry entry : _compare.entrySet()) { + int idx = entry.getKey(); + if (idx >= parameters.length || idx >= dataType.size()) { + return false; } - if (dataType.get(i) instanceof Class clz && clz.isAssignableFrom(String.class)) { - String compareVal = _compare.get(i); - return compareVal == parameters[i] || compareVal.equals(parameters[i]); + if (!(dataType.get(idx) instanceof Class clz && clz.isAssignableFrom(String.class))) { + return false; + } + if (!entry.getValue().equals(parameters[idx])) { + return false; } } + return true; } catch (DBusException _ex) { throw new DBusExecutionException("Unable to get parameters from signal", _ex); } - - return false; } /** @@ -101,28 +104,32 @@ static boolean matchArg0123Path(Message _msg, Map _compare) { Object[] parameters = _msg.getParameters(); - for (int i = 0; i < parameters.length; i++) { - if (!_compare.containsKey(i)) { - continue; + // all configured argument-path constraints must match (logical AND) + for (Map.Entry entry : _compare.entrySet()) { + int idx = entry.getKey(); + if (idx >= parameters.length || idx >= dataType.size()) { + return false; + } + if (!(dataType.get(idx) instanceof Class clz)) { + return false; } - if (dataType.get(i) instanceof Class clz) { - String matchVal; - if (clz.isAssignableFrom(String.class)) { - matchVal = (String) parameters[i]; - } else if (clz.isAssignableFrom(DBusPath.class)) { - matchVal = ((DBusPath) parameters[i]).getPath(); - } else { - continue; // not String or DBusPath, do not try to match - } - String compareVal = _compare.get(i); - return compareVal == matchVal || matchesArg0Path(matchVal, compareVal); + String matchVal; + if (clz.isAssignableFrom(String.class)) { + matchVal = (String) parameters[idx]; + } else if (clz.isAssignableFrom(DBusPath.class)) { + matchVal = ((DBusPath) parameters[idx]).getPath(); + } else { + return false; // not String or DBusPath, cannot match this arg + } + String compareVal = entry.getValue(); + if (!(compareVal.equals(matchVal) || matchesArg0Path(matchVal, compareVal))) { + return false; } } + return true; } catch (DBusException _ex) { throw new DBusExecutionException("Unable to get parameters from signal", _ex); } - - return false; } private static boolean matchesArg0Path(String _matchVal, String _compareVal) { @@ -145,7 +152,7 @@ private static boolean matchesArg0Path(String _matchVal, String _compareVal) { * @see DBus Specification */ static boolean matchPathNamespace(String _input, String _compare) { - if (DBusObjects.validateNotObjectPath(_compare) || DBusObjects.validateNotObjectPath(_compare)) { + if (DBusObjects.validateNotObjectPath(_input) || DBusObjects.validateNotObjectPath(_compare)) { return false; } else if (!_input.startsWith(_compare)) { return false; diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleMatcherTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleMatcherTest.java index 208630a8e..1de32a01b 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleMatcherTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleMatcherTest.java @@ -4,6 +4,7 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.messages.Message; import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -62,11 +63,25 @@ public Object[] getParameters() throws DBusException { assertEquals(_matchResult, MatchRuleMatcher.matchArg0123(msg, _matcher)); } + @Test + void testMatchPathNamespace() { + assertTrue(MatchRuleMatcher.matchPathNamespace("/com/example/foo", "/com/example")); + assertTrue(MatchRuleMatcher.matchPathNamespace("/com/example", "/com/example")); + assertFalse(MatchRuleMatcher.matchPathNamespace("/com/example/foobar", "/com/example/foo")); + // an input that is not a valid object path must never match (previously _input was not validated) + assertFalse(MatchRuleMatcher.matchPathNamespace("/com//example", "/com")); + } + static Stream createArg0123TestData() { return Stream.of( Arguments.arguments(List.of("test"), Map.of(0, "test"), true), Arguments.arguments(List.of("test", 1), Map.of(0, "foo"), false), - Arguments.arguments(List.of("test"), Map.of(0, "te"), false) + Arguments.arguments(List.of("test"), Map.of(0, "te"), false), + // multiple arg constraints must all match (logical AND) + Arguments.arguments(List.of("test", "foo"), Map.of(0, "test", 1, "foo"), true), + Arguments.arguments(List.of("test", "foo"), Map.of(0, "test", 1, "nomatch"), false), + // arg index beyond the actual parameters must not match + Arguments.arguments(List.of("a"), Map.of(0, "a", 3, "b"), false) ); } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleParserTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleParserTest.java index 5981fa637..e162670be 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleParserTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/matchrules/MatchRuleParserTest.java @@ -1,8 +1,11 @@ package org.freedesktop.dbus.matchrules; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.freedesktop.dbus.errors.MatchRuleInvalid; import org.freedesktop.dbus.messages.constants.MessageTypes; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -12,6 +15,19 @@ public class MatchRuleParserTest { + @Test + void testConvertRuleWithArgPath() { + // parsing an argNpath key used to throw (wrong regex group was read) -> now yields the value + DBusMatchRule rule = MatchRuleParser.convertMatchRule("type='signal',arg0path='/org/example/Foo'"); + assertEquals(Map.of(0, "/org/example/Foo"), rule.getArg0123Path()); + } + + @Test + void testConvertRuleRejectsTooLargeArgIndex() { + // spec allows arg indexes 0..63 only; larger indexes must be rejected instead of silently accepted + assertThrows(MatchRuleInvalid.class, () -> MatchRuleParser.convertMatchRule("type='signal',arg64='x'")); + } + @ParameterizedTest @MethodSource("createDBusRuleTestData") void testParseFromDBusRule(DBusMatchRule _testStr, Map _results) { From 765fc8e2eddb422fdec16e1223f353fcd1b84fa3 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 21:47:09 +0200 Subject: [PATCH 20/38] Fixed SASL FileDescriptor handling --- .../freedesktop/dbus/connections/SASL.java | 47 ++++++++------ .../SaslFileDescriptorNegotiationTest.java | 63 +++++++++++++++++++ 2 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index 2a3cf5925..1fa61fa59 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -544,22 +544,15 @@ public boolean auth(SocketChannel _sock, AbstractTransport _transport) throws IO } break; case ERROR: - // when asking for file descriptor support, ERROR means FD support is not supported - if (state == SaslAuthState.NEGOTIATE_UNIX_FD) { - state = SaslAuthState.FINISHED; - logger.trace("File descriptors NOT supported by server"); - fileDescriptorSupported = false; - send(_sock, BEGIN); - } else { - send(_sock, CANCEL); - state = SaslAuthState.WAIT_REJECT; - } + // ERROR during the authentication exchange -> abort and wait for REJECTED + send(_sock, CANCEL); + state = SaslAuthState.WAIT_REJECT; break; case OK: logger.trace("Authenticated"); if (saslConfig.isFileDescriptorSupport()) { - state = SaslAuthState.WAIT_DATA; + state = SaslAuthState.NEGOTIATE_UNIX_FD; logger.trace("Asking for file descriptor support"); // if authentication was successful, ask remote end for file descriptor support send(_sock, NEGOTIATE_UNIX_FD); @@ -568,18 +561,31 @@ public boolean auth(SocketChannel _sock, AbstractTransport _transport) throws IO send(_sock, BEGIN); } break; + default: + send(_sock, ERROR, INVALID_CMD_ERR); + break; + } + break; + case NEGOTIATE_UNIX_FD: + c = receive(_sock); + switch (c.getCommand()) { case AGREE_UNIX_FD: - if (saslConfig.isFileDescriptorSupport()) { - state = SaslAuthState.FINISHED; - logger.trace("File descriptors supported by server"); - fileDescriptorSupported = true; - send(_sock, BEGIN); - } + logger.trace("File descriptors supported by server"); + fileDescriptorSupported = true; + send(_sock, BEGIN); + state = SaslAuthState.FINISHED; + break; + case ERROR: + // server does not support unix fd passing -> continue gracefully without it + logger.trace("File descriptors NOT supported by server"); + fileDescriptorSupported = false; + send(_sock, BEGIN); + state = SaslAuthState.FINISHED; break; default: send(_sock, ERROR, INVALID_CMD_ERR); break; - } + } break; case WAIT_OK: c = receive(_sock); @@ -844,7 +850,7 @@ public Command(String _s) throws IOException { LoggingHelper.logIf(logger.isTraceEnabled(), () -> logger.trace("Creating command from: {}", Arrays.toString(ss))); if (0 == COL.compare(ss[0], "OK")) { command = OK; - data = ss[1]; + data = ss.length < 2 ? null : ss[1]; } else if (0 == COL.compare(ss[0], "AUTH")) { command = AUTH; if (ss.length > 1) { @@ -880,7 +886,8 @@ public Command(String _s) throws IOException { command = CANCEL; } else if (0 == COL.compare(ss[0], "ERROR")) { command = ERROR; - data = ss[1]; + // the error message is optional per the D-Bus spec (e.g. a bare "ERROR") + data = ss.length < 2 ? null : ss[1]; } else if (0 == COL.compare(ss[0], "NEGOTIATE_UNIX_FD")) { command = NEGOTIATE_UNIX_FD; } else if (0 == COL.compare(ss[0], "AGREE_UNIX_FD")) { diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java new file mode 100644 index 000000000..5adb9b0d9 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java @@ -0,0 +1,63 @@ +package org.freedesktop.dbus.connections.config; + +import org.freedesktop.dbus.connections.SASL; +import org.freedesktop.dbus.connections.SASL.SaslMode; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Timeout; + +/** + * Verifies that a SASL client which requested unix file descriptor support still completes the + * authentication (without fd support) when the server rejects the {@code NEGOTIATE_UNIX_FD} request + * with {@code ERROR}. Lives in the {@code ...config} package to access the package-private + * {@link SaslConfig} constructor; only the public {@link SASL} API is exercised. + */ +class SaslFileDescriptorNegotiationTest extends AbstractBaseTest { + + @Test + @Timeout(value = 15, unit = TimeUnit.SECONDS) + void testClientContinuesWhenServerRejectsUnixFd() throws Exception { + try (ServerSocketChannel ssc = ServerSocketChannel.open()) { + ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + + SaslConfig serverCfg = new SaslConfig(); + serverCfg.setMode(SaslMode.SERVER); + serverCfg.setAuthMode(SASL.AUTH_ANON); + serverCfg.setGuid("00000000000000000000000000000000"); + serverCfg.setFileDescriptorSupport(false); // server rejects NEGOTIATE_UNIX_FD with ERROR + + SaslConfig clientCfg = new SaslConfig(); + clientCfg.setMode(SaslMode.CLIENT); + clientCfg.setAuthMode(SASL.AUTH_ANON); + clientCfg.setFileDescriptorSupport(true); // client requests fd support + + try (ExecutorService exec = Executors.newSingleThreadExecutor()) { + Future serverFuture = exec.submit(() -> { + try (SocketChannel serverCh = ssc.accept()) { + return new SASL(serverCfg).auth(serverCh, null); + } + }); + + SASL clientSasl = new SASL(clientCfg); + try (SocketChannel clientCh = SocketChannel.open(ssc.getLocalAddress())) { + boolean clientResult = clientSasl.auth(clientCh, null); + assertTrue(clientResult, + "client auth should succeed even when the server rejects unix fd support"); + assertFalse(clientSasl.isFileDescriptorSupported(), + "fd support must be disabled after the server replied ERROR to NEGOTIATE_UNIX_FD"); + assertTrue(serverFuture.get(10, TimeUnit.SECONDS), "server auth should succeed"); + } + } + } + } +} From 74de2cbe27dd4e36c6ddc54bdce206b8c34e46b3 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 22:31:39 +0200 Subject: [PATCH 21/38] Fixed checkstyle issues --- .../dbus/connections/AbstractConnection.java | 13 ++++++--- .../freedesktop/dbus/connections/SASL.java | 27 +++++++++++++++---- .../base/IncomingMessageThread.java | 1 - .../dbus/connections/impl/DBusConnection.java | 19 +++++++++---- .../impl/DBusConnectionBuilder.java | 3 ++- .../connections/impl/DirectConnection.java | 6 ++--- .../transports/TransportConnection.java | 6 +++-- .../freedesktop/dbus/messages/Message.java | 3 +-- .../freedesktop/dbus/messages/MethodCall.java | 2 +- .../java/org/freedesktop/dbus/utils/Util.java | 6 ++--- .../SaslFileDescriptorNegotiationTest.java | 3 +-- .../dbus/test/DisconnectCallbackTest.java | 1 - .../transport/tcp/TcpTransportProvider.java | 2 +- .../generator/GeneratedCodeCompiler.java | 1 + .../generator/InterfaceCodeGeneratorTest.java | 13 ++++----- 15 files changed, 68 insertions(+), 38 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index d0b9d9ccd..7a6e09ac3 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -1,7 +1,5 @@ package org.freedesktop.dbus.connections; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; import org.freedesktop.dbus.DBusAsyncReply; import org.freedesktop.dbus.RemoteInvocationHandler; import org.freedesktop.dbus.RemoteObject; @@ -21,12 +19,19 @@ import org.freedesktop.dbus.messages.ExportedObject; import org.freedesktop.dbus.messages.MethodCall; import org.freedesktop.dbus.utils.DBusObjects; +import org.freedesktop.dbus.utils.IThrowingRunnable; import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; -import org.freedesktop.dbus.utils.IThrowingRunnable; /** * Handles a connection to DBus. diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index 1fa61fa59..f27cb360a 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -1,9 +1,16 @@ package org.freedesktop.dbus.connections; -import static org.freedesktop.dbus.connections.SASL.SaslCommand.*; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.AGREE_UNIX_FD; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.AUTH; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.BEGIN; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.CANCEL; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.DATA; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.ERROR; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.NEGOTIATE_UNIX_FD; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.OK; +import static org.freedesktop.dbus.connections.SASL.SaslCommand.REJECTED; import com.sun.security.auth.module.UnixSystem; -import java.nio.file.StandardCopyOption; import org.freedesktop.dbus.config.DBusSysProps; import org.freedesktop.dbus.connections.config.SaslConfig; import org.freedesktop.dbus.connections.transports.AbstractTransport; @@ -18,7 +25,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; @@ -26,13 +37,19 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermission; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.Collator; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; public class SASL { public static final int AUTH_NONE = 0; @@ -218,7 +235,7 @@ private void addCookie(String _context, String _id, long _timestamp, String _coo // atomically move to old file try { Files.move(temp.toPath(), cookiefile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { + } catch (IOException _ex) { logger.warn("Unable to atomically move cookie file {} to {}", temp, cookiefile); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java index 600e1520a..7ed0dd0ff 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/IncomingMessageThread.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.util.Objects; -import java.util.concurrent.RejectedExecutionException; public class IncomingMessageThread extends Thread { private final Logger logger = LoggerFactory.getLogger(getClass()); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java index 99a3d5632..c8faab04c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java @@ -1,6 +1,8 @@ package org.freedesktop.dbus.connections.impl; -import static org.freedesktop.dbus.utils.CommonRegexPattern.*; +import static org.freedesktop.dbus.utils.CommonRegexPattern.DBUS_IFACE_PATTERN; +import static org.freedesktop.dbus.utils.CommonRegexPattern.IFACE_PATTERN; +import static org.freedesktop.dbus.utils.CommonRegexPattern.PROXY_SPLIT_PATTERN; import org.freedesktop.dbus.RemoteInvocationHandler; import org.freedesktop.dbus.RemoteObject; @@ -8,7 +10,11 @@ import org.freedesktop.dbus.connections.IDisconnectAction; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; -import org.freedesktop.dbus.exceptions.*; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.exceptions.InvalidBusNameException; +import org.freedesktop.dbus.exceptions.InvalidObjectPathException; +import org.freedesktop.dbus.exceptions.NotConnected; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.DBusSigHandler; @@ -24,11 +30,14 @@ import java.io.IOException; import java.lang.reflect.Proxy; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java index b24b4d795..f829d59df 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java @@ -2,7 +2,6 @@ import static org.freedesktop.dbus.utils.AddressBuilder.getDbusMachineId; -import java.util.Optional; import org.freedesktop.dbus.connections.BusAddress; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; @@ -12,6 +11,8 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.utils.AddressBuilder; +import java.util.Optional; + /** * Builder to create a new DBusConnection. * diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java index a18f325c0..389a18df2 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnection.java @@ -26,8 +26,6 @@ import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; /** * Handles a peer to peer connection between two applications without a bus daemon. @@ -37,8 +35,8 @@ */ public class DirectConnection extends AbstractConnection { private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final String machineId; + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final String machineId; DirectConnection(ConnectionConfig _conCfg, TransportConfig _transportCfg, ReceivingServiceConfig _rsCfg) throws DBusException { super(_conCfg, _transportCfg, _rsCfg); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java index 6158a0bd2..15062f5c4 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportConnection.java @@ -1,13 +1,15 @@ package org.freedesktop.dbus.connections.transports; import org.freedesktop.dbus.messages.MessageFactory; -import org.freedesktop.dbus.spi.message.*; +import org.freedesktop.dbus.spi.message.IMessageReader; +import org.freedesktop.dbus.spi.message.IMessageWriter; +import org.freedesktop.dbus.spi.message.ISocketProvider; +import org.freedesktop.dbus.utils.Util; import java.io.Closeable; import java.io.IOException; import java.nio.channels.SocketChannel; import java.util.concurrent.atomic.AtomicLong; -import org.freedesktop.dbus.utils.Util; /** * Represents one transport connection of any type.
diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java index e5808a8b7..4ca32515c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java @@ -1033,7 +1033,6 @@ private Object extractStruct(byte[] _signatureBuf, byte[] _dataBuf, int[] _offse */ private Object extractArray(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, ExtractOptions _options, ExtractMethod _extractMethod) throws MarshallingException, DBusException { - Object rv; long size = demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4); logger.trace("Reading array of size: {}", size); @@ -1049,7 +1048,7 @@ private Object extractArray(byte[] _signatureBuf, byte[] _dataBuf, int[] _offset } int length = (int) (size / algn); - rv = optimizePrimitives(_signatureBuf, _dataBuf, _offsets, size, algn, length, _options, _extractMethod); + Object rv = optimizePrimitives(_signatureBuf, _dataBuf, _offsets, size, algn, length, _options, _extractMethod); if (_options.contained() && !(rv instanceof List) && !(rv instanceof Map)) { rv = ArrayFrob.listify(rv); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java index 7269f2bb9..feda63a9e 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/MethodCall.java @@ -1,6 +1,5 @@ package org.freedesktop.dbus.messages; -import java.util.concurrent.TimeUnit; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.MessageFormatException; import org.freedesktop.dbus.messages.constants.ArgumentType; @@ -10,6 +9,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; public class MethodCall extends MethodBase { private static long replyWaitTimeout = Duration.ofSeconds(20).toMillis(); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java index dbd14a407..231a80db2 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/Util.java @@ -62,7 +62,7 @@ private Util() {} */ public static Properties readProperties(File _file) { if (_file.exists()) { - try (FileInputStream fis = new FileInputStream(_file)){ + try (FileInputStream fis = new FileInputStream(_file)) { return readProperties(fis); } catch (IOException _ex) { LOGGER.info("Could not load properties file: {}", _file, _ex); @@ -747,8 +747,8 @@ public static void closeQuietly(Closeable... _closeables) { if (c != null) { try { c.close(); - } catch (Exception e) { - LOGGER.debug("Failed to close {}", c, e); + } catch (Exception _ex) { + LOGGER.debug("Failed to close {}", c, _ex); } } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java index 5adb9b0d9..a297cf372 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/config/SaslFileDescriptorNegotiationTest.java @@ -4,17 +4,16 @@ import org.freedesktop.dbus.connections.SASL.SaslMode; import org.freedesktop.dbus.test.AbstractBaseTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; -import java.time.Duration; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Timeout; /** * Verifies that a SASL client which requested unix file descriptor support still completes the diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java index bf26140ab..23d4cbb70 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/DisconnectCallbackTest.java @@ -1,6 +1,5 @@ package org.freedesktop.dbus.test; -import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.CallbackHandler; import org.freedesktop.dbus.test.helper.interfaces.SlowInterface; diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java index 52112e141..279b73d73 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java @@ -1,6 +1,5 @@ package org.freedesktop.dbus.transport.tcp; -import java.security.SecureRandom; import org.freedesktop.dbus.connections.BusAddress; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractTransport; @@ -10,6 +9,7 @@ import org.slf4j.LoggerFactory; import java.net.ServerSocket; +import java.security.SecureRandom; import java.util.Random; public class TcpTransportProvider implements ITransportProvider { diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java index 2092fcb2f..5e10e13ec 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/GeneratedCodeCompiler.java @@ -16,6 +16,7 @@ import java.util.Map.Entry; import java.util.stream.Collectors; import java.util.stream.Stream; + import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; import javax.tools.FileObject; diff --git a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java index 0ed6586b2..53324345a 100644 --- a/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java +++ b/dbus-java-utils/src/test/java/org/freedesktop/dbus/utils/generator/InterfaceCodeGeneratorTest.java @@ -3,6 +3,13 @@ import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; import static org.junit.jupiter.api.Assertions.*; +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.utils.Util; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + import java.io.File; import java.util.Arrays; import java.util.List; @@ -10,12 +17,6 @@ import java.util.Map.Entry; import java.util.Set; import java.util.stream.Stream; -import org.freedesktop.dbus.annotations.DBusInterfaceName; -import org.freedesktop.dbus.utils.Util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; class InterfaceCodeGeneratorTest { From 028df8773fd39ca4049ec0fd29c7bbf332cf5029 Mon Sep 17 00:00:00 2001 From: David M Date: Fri, 17 Jul 2026 22:34:35 +0200 Subject: [PATCH 22/38] documentation clarification --- README.md | 6 ++++++ src/site/markdown/dbus-types.md | 19 ++++++++++++++----- src/site/markdown/quick-start.md.vm | 7 +++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b992d3ef6..ebf54c508 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ To use file descriptors with dbus-java version 4.x before 4.3.1 you have to do t When using dbus-java-nativefd, you have to use version 2.x when using dbus-java 4.x/5.x and 1.x if you use dbus-java 3.x. DBus-java will automatically detect dbus-java-nativefd and will then provide access to file descriptors. +Please note that `dbus-java-nativefd` is a third-party library which is **not** maintained by the dbus-java project. +It relies on platform-specific native (JNI/C) code and is therefore not necessarily portable to every architecture. +For this reason it is intentionally not bundled with dbus-java. Whenever possible, prefer the +`dbus-java-transport-junixsocket` transport, which supports file descriptor passing without any additional dependency. +The `dbus-java-transport-native-unixsocket` transport does not support file descriptor passing at all. + If you are using version 4.3.1 or higher, you may simple switch to `dbus-java-transport-junixsocket` (instead of `dbus-java-transport-jnr-unixsocket` or `dbus-java-transport-native-unixsocket`). You do this by adding `dbus-java-transport-junixsocket` to your classpath. Remember to remove the other unixsocket implementations because you are not allowed to have multiple implementations of the same protocol at once. diff --git a/src/site/markdown/dbus-types.md b/src/site/markdown/dbus-types.md index 2e3c30bc0..a9ea12ef9 100644 --- a/src/site/markdown/dbus-types.md +++ b/src/site/markdown/dbus-types.md @@ -22,11 +22,20 @@ The following table contains a mapping of DBus types to DBus-Java types |{} dictionary |java.util.Map| |h |org.freedesktop.dbus.FileDescriptor*| -*File descriptor passing (`h`) requires a transport that supports it. When using the -`dbus-java-transport-junixsocket` transport (recommended, available since dbus-java 4.3.1), -file descriptors work out of the box - no additional dependency is required. -See the [README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors) for -details and for the legacy setup using the `dbus-java-transport-jnr-unixsocket` transport. +*File descriptor passing (`h`) depends on the transport you use: + + * `dbus-java-transport-junixsocket` - **recommended**; file descriptors work out of the box + (since dbus-java 4.3.1), no additional dependency required. + * `dbus-java-transport-native-unixsocket` - does **not** support file descriptor passing. + * `dbus-java-transport-jnr-unixsocket` - supports file descriptors only in combination with an + additional, third-party native library + ([com.rm5248:dbus-java-nativefd](https://github.com/rm5248/dbus-java-nativefd)). That library + relies on platform-specific native (JNI/C) code, is therefore **not** architecture-portable, + and is **not** shipped or maintained by the dbus-java project. If you need file descriptor + passing, prefer the `junixsocket` transport instead. + * `dbus-java-transport-tcp` - file descriptor passing is not possible over TCP. + +See the [README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors) for setup details. ## Examples diff --git a/src/site/markdown/quick-start.md.vm b/src/site/markdown/quick-start.md.vm index 2586efad3..78f24e0ee 100644 --- a/src/site/markdown/quick-start.md.vm +++ b/src/site/markdown/quick-start.md.vm @@ -39,6 +39,13 @@ For maven this would look like this: ${project.version} ``` + +If your application needs to pass **unix file descriptors**, choose the `dbus-java-transport-junixsocket` +transport - it supports file descriptor passing out of the box. The `native-unixsocket` transport does not +support file descriptors, and `jnr-unixsocket` only does so via an additional, unmaintained third-party +native library (see the [README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors)). +File descriptors cannot be passed over TCP. + 2. (optional) Add in a logging framework of your choice to see the log messages. DBus-Java uses SLF4J internally for logging. ``` From 26c67b4bbcf76d4d487adccd77a9e693c2cfd5b5 Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 18:58:51 +0200 Subject: [PATCH 23/38] Fixed: PropertiesChanged signal was not emitted when using bound properties --- .../base/DBusBoundPropertyHandler.java | 130 +++++++++++++++++- .../impl/BaseConnectionBuilder.java | 18 +++ .../connections/impl/ConnectionConfig.java | 9 ++ .../dbus/test/BoundPropertiesTest.java | 117 ++++++++++++++++ src/site/markdown/properties.md | 24 ++++ 5 files changed, 297 insertions(+), 1 deletion(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java index 963822bab..e2eed6607 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java @@ -5,6 +5,8 @@ import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.annotations.PropertiesEmitsChangedSignal; +import org.freedesktop.dbus.annotations.PropertiesEmitsChangedSignal.EmitChangeSignal; import org.freedesktop.dbus.connections.AbstractConnection; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; @@ -246,7 +248,12 @@ protected PropHandled handleSet(ExportedObject _exportObject, final MethodCall _ } } _methodCall.setArgs(Marshalling.deSerializeParameters(new Object[] {myVal}, new Type[] {type}, this, true)); - invokeMethodAndReply(_methodCall, propMeth, object, 1 == (_methodCall.getFlags() & Flags.NO_REPLY_EXPECTED)); + boolean noReply = 1 == (_methodCall.getFlags() & Flags.NO_REPLY_EXPECTED); + if (invokeSetterAndReply(_methodCall, propMeth, object, noReply)) { + // property was changed successfully; optionally announce it via PropertiesChanged + emitPropertiesChangedIfEnabled(_exportObject, _methodCall, propMeth, + (String) _params[0], (String) _params[1], myVal); + } } catch (Exception _ex) { getLogger().debug("Failed to invoke method call on Properties", _ex); handleException(_methodCall, new UnknownMethod("Failure in de-serializing message: " + _ex)); @@ -258,6 +265,127 @@ protected PropHandled handleSet(ExportedObject _exportObject, final MethodCall _ } + /** + * Invokes a property setter and sends the (void) method reply, mirroring the error handling of + * {@link ConnectionMethodInvocation#invokeMethodAndReply(MethodCall, Method, Object, boolean)} but + * reporting whether the setter completed successfully so the caller can decide whether to emit a + * PropertiesChanged signal. + * + * @param _methodCall the Set method call + * @param _setter the setter method + * @param _object the exported object instance + * @param _noReply whether a reply is expected + * + * @return {@code true} if the setter was invoked without error, {@code false} otherwise + */ + private boolean invokeSetterAndReply(MethodCall _methodCall, Method _setter, Object _object, boolean _noReply) { + try { + invokeMethod(_methodCall, _setter, _object); + if (!_noReply) { + invokedMethodReply(_methodCall, _setter, null); + } + return true; + } catch (DBusExecutionException _ex) { + getLogger().debug("Failed to invoke property setter", _ex); + handleException(_methodCall, _ex); + } catch (Throwable _ex) { + getLogger().debug("Error invoking property setter {}", _methodCall, _ex); + handleException(_methodCall, new DBusExecutionException(String.format("Error Executing Method %s.%s: %s", + _methodCall.getInterface(), _methodCall.getName(), _ex.getMessage()), _ex)); + } + return false; + } + + /** + * Emits an {@code org.freedesktop.DBus.Properties.PropertiesChanged} signal for a changed bound + * property, if enabled on the connection ({@link ConnectionConfig#isAutoEmitPropertiesChanged()}) + * and permitted by the property's {@link EmitChangeSignal} value. + * + * @param _exportObject the exported object + * @param _methodCall the originating Set call (used for the object path) + * @param _setter the property setter method (source of the EmitChangeSignal annotation) + * @param _propertyInterface the interface name the property belongs to (from the Set arguments) + * @param _propertyName the property name + * @param _setValue the value that was set (fallback when no getter is available) + */ + private void emitPropertiesChangedIfEnabled(ExportedObject _exportObject, MethodCall _methodCall, Method _setter, + String _propertyInterface, String _propertyName, Object _setValue) { + if (!getConnectionConfig().isAutoEmitPropertiesChanged()) { + return; + } + EmitChangeSignal emit = resolveEmitChangeSignal(_setter); + if (emit == EmitChangeSignal.CONST || emit == EmitChangeSignal.FALSE) { + return; + } + + try { + Map> changed = new HashMap<>(); + List invalidated = new ArrayList<>(); + + if (emit == EmitChangeSignal.INVALIDATES) { + invalidated.add(_propertyName); + } else { // TRUE - include the current value + Method getter = _exportObject.getPropertyMethods().get(new PropertyRef(_propertyName, null, Access.READ)); + Object value; + Type valueType; + if (getter != null) { + value = getter.invoke(_exportObject.getObject().get()); + valueType = getter.getGenericReturnType(); + } else { // write-only property - fall back to the value that was set + value = _setValue; + valueType = _setter.getGenericParameterTypes()[0]; + } + if (value == null) { + getLogger().debug("Not emitting PropertiesChanged for {}.{}: value is null", _propertyInterface, _propertyName); + return; + } + changed.put(_propertyName, toVariant(value, valueType)); + } + + sendMessage(new Properties.PropertiesChanged(_methodCall.getPath(), _propertyInterface, changed, invalidated)); + } catch (Exception _ex) { + getLogger().warn("Failed to emit PropertiesChanged for property {}.{}", _propertyInterface, _propertyName, _ex); + } + } + + /** + * Resolves the effective {@link EmitChangeSignal} for a bound property. A non-default value on the + * {@link DBusBoundProperty#emitChangeSignal()} method annotation takes precedence, followed by the + * interface-global {@link PropertiesEmitsChangedSignal} annotation, defaulting to {@link EmitChangeSignal#TRUE}. + * + * @param _setter setter method of the property + * @return effective EmitChangeSignal + */ + private EmitChangeSignal resolveEmitChangeSignal(Method _setter) { + DBusBoundProperty boundProperty = _setter.getAnnotation(DBusBoundProperty.class); + if (boundProperty != null && boundProperty.emitChangeSignal() != EmitChangeSignal.TRUE) { + return boundProperty.emitChangeSignal(); + } + PropertiesEmitsChangedSignal global = _setter.getDeclaringClass().getAnnotation(PropertiesEmitsChangedSignal.class); + if (global != null) { + return global.value(); + } + return EmitChangeSignal.TRUE; + } + + /** + * Wraps a property value in a {@link Variant}, computing the DBus signature for array/collection/map + * values from the given type (as {@code GetAll} does). + * + * @param _value value to wrap (must not be {@code null}) + * @param _type generic type of the value (getter return type or setter parameter type) + * @return the wrapped variant + * + * @throws DBusException when the DBus type cannot be determined + */ + private Variant toVariant(Object _value, Type _type) throws DBusException { + if (_value.getClass().isArray() || _value instanceof Collection || _value instanceof Map) { + String signature = String.join("", Marshalling.getDBusType(_type)); + return new Variant<>(_value, signature); + } + return new Variant<>(_value); + } + enum PropHandled { /** Property request was handled. */ HANDLED, diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java index b27d48930..98fc2dba3 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java @@ -169,6 +169,24 @@ public R withUnknownSignalHandler(Consumer _handler) { return self(); } + /** + * Enables automatic emission of the {@code org.freedesktop.DBus.Properties.PropertiesChanged} + * signal whenever a property backed by {@link org.freedesktop.dbus.annotations.DBusBoundProperty} + * is successfully changed via the DBus {@code Properties.Set} method. + *

+ * Disabled by default for backwards compatibility. When enabled, the actual emission behavior of + * each property is controlled by its + * {@link org.freedesktop.dbus.annotations.PropertiesEmitsChangedSignal.EmitChangeSignal} value + * ({@code TRUE} = emit with value, {@code INVALIDATES} = emit without value, {@code CONST}/{@code FALSE} = no signal). + *

+ * @param _autoEmit true to enable automatic PropertiesChanged emission + * @return this + */ + public R withAutoEmitPropertiesChanged(boolean _autoEmit) { + connectionConfig.setAutoEmitPropertiesChanged(_autoEmit); + return self(); + } + public abstract C build() throws DBusException; /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java index 283b1c92a..0ce2ecc5c 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java @@ -10,6 +10,7 @@ public class ConnectionConfig { private boolean importWeakReferences; private IDisconnectCallback disconnectCallback; private Consumer unknownSignalHandler; + private boolean autoEmitPropertiesChanged; public boolean isExportWeakReferences() { return exportWeakReferences; @@ -43,4 +44,12 @@ public void setUnknownSignalHandler(Consumer _unknownSignalHandler) unknownSignalHandler = _unknownSignalHandler; } + public boolean isAutoEmitPropertiesChanged() { + return autoEmitPropertiesChanged; + } + + public void setAutoEmitPropertiesChanged(boolean _autoEmitPropertiesChanged) { + autoEmitPropertiesChanged = _autoEmitPropertiesChanged; + } + } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java index b5f7ed72d..ead678867 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/BoundPropertiesTest.java @@ -4,6 +4,7 @@ import org.freedesktop.dbus.annotations.DBusBoundProperty; import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.annotations.PropertiesEmitsChangedSignal.EmitChangeSignal; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; @@ -17,6 +18,9 @@ import java.io.IOException; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; public class BoundPropertiesTest extends AbstractDBusDaemonBaseTest { @@ -151,6 +155,119 @@ public void testMixedProperties() throws IOException, DBusException { } } + @Test + public void testAutoEmitPropertiesChangedWhenEnabled() throws Exception { + try (DBusConnection conn = DBusConnectionBuilder.forSessionBus().withShared(false) + .withAutoEmitPropertiesChanged(true).build()) { + MyObject obj = new MyObject(); + conn.requestBusName("com.acme"); + conn.exportObject(obj); + + try (DBusConnection innerConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference received = new AtomicReference<>(); + + try (AutoCloseable handler = innerConn.addSigHandler(Properties.PropertiesChanged.class, sig -> { + if ("/com/acme/MyObject".equals(sig.getPath())) { + received.set(sig); + latch.countDown(); + } + })) { + MyInterface myObject = innerConn.getRemoteObject("com.acme", "/com/acme/MyObject", MyInterface.class); + myObject.setMyProperty("New value"); + + assertTrue(latch.await(10, TimeUnit.SECONDS), "PropertiesChanged signal was not received"); + + Properties.PropertiesChanged sig = received.get(); + assertEquals("com.acme.MyInterface", sig.getInterfaceName()); + assertTrue(sig.getPropertiesChanged().containsKey("MyProperty"), "changed map must contain the property"); + assertEquals("New value", sig.getPropertiesChanged().get("MyProperty").getValue()); + assertTrue(sig.getPropertiesRemoved().isEmpty(), "no invalidated properties expected"); + } + } + } + } + + @Test + public void testNoPropertiesChangedWhenDisabledByDefault() throws Exception { + try (DBusConnection conn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + MyObject obj = new MyObject(); + conn.requestBusName("com.acme"); + conn.exportObject(obj); + + try (DBusConnection innerConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + CountDownLatch latch = new CountDownLatch(1); + + try (AutoCloseable handler = innerConn.addSigHandler(Properties.PropertiesChanged.class, sig -> { + if ("/com/acme/MyObject".equals(sig.getPath())) { + latch.countDown(); + } + })) { + MyInterface myObject = innerConn.getRemoteObject("com.acme", "/com/acme/MyObject", MyInterface.class); + myObject.setMyProperty("New value"); + + assertFalse(latch.await(2, TimeUnit.SECONDS), + "no PropertiesChanged signal expected when auto-emit is disabled (default)"); + } + } + } + } + + @Test + public void testNoPropertiesChangedForFalseAnnotationEvenWhenEnabled() throws Exception { + try (DBusConnection conn = DBusConnectionBuilder.forSessionBus().withShared(false) + .withAutoEmitPropertiesChanged(true).build()) { + SilentPropObject obj = new SilentPropObject(); + conn.requestBusName("com.acme.silent"); + conn.exportObject(obj); + + try (DBusConnection innerConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + CountDownLatch latch = new CountDownLatch(1); + + try (AutoCloseable handler = innerConn.addSigHandler(Properties.PropertiesChanged.class, sig -> { + if ("/com/acme/silent/SilentObject".equals(sig.getPath())) { + latch.countDown(); + } + })) { + SilentPropInterface myObject = innerConn.getRemoteObject("com.acme.silent", + "/com/acme/silent/SilentObject", SilentPropInterface.class); + myObject.setSilentProperty("New value"); + + assertFalse(latch.await(2, TimeUnit.SECONDS), + "no PropertiesChanged signal expected for a property annotated with emitChangeSignal=FALSE"); + } + } + } + } + + @DBusInterfaceName("com.acme.silent.SilentPropInterface") + public interface SilentPropInterface extends DBusInterface { + @DBusBoundProperty(access = Access.READ, name = "SilentProperty", emitChangeSignal = EmitChangeSignal.FALSE) + String getSilentProperty(); + + @DBusBoundProperty(access = Access.WRITE, name = "SilentProperty", emitChangeSignal = EmitChangeSignal.FALSE) + void setSilentProperty(String _property); + } + + public static class SilentPropObject implements SilentPropInterface { + private String silentProperty = "Initial value"; + + @Override + public String getSilentProperty() { + return silentProperty; + } + + @Override + public void setSilentProperty(String _property) { + silentProperty = _property; + } + + @Override + public String getObjectPath() { + return "/com/acme/silent/SilentObject"; + } + } + @DBusInterfaceName("com.acme.mixed.MixedProperties") public interface MixedProperties extends DBusInterface, Properties { @DBusBoundProperty(access = Access.READ, name = "AnnotationProperty") diff --git a/src/site/markdown/properties.md b/src/site/markdown/properties.md index 7b52a3861..ab4306169 100644 --- a/src/site/markdown/properties.md +++ b/src/site/markdown/properties.md @@ -172,6 +172,30 @@ public class ExportMyObject { } ``` +#### Automatic `PropertiesChanged` signals + +By default dbus-java does **not** emit the `org.freedesktop.DBus.Properties.PropertiesChanged` +signal when a bound property is changed via `Properties.Set` (kept off for backwards +compatibility). You can enable this on the connection builder: + +```java +DBusConnection conn = DBusConnectionBuilder.forSessionBus() + .withAutoEmitPropertiesChanged(true) + .build(); +``` + +When enabled, the emission is controlled per property by the +`org.freedesktop.DBus.Property.EmitsChangedSignal` annotation +(`@DBusBoundProperty(emitChangeSignal = ...)`, or the interface-wide +`@PropertiesEmitsChangedSignal`): + + * `TRUE` (default) - emit `PropertiesChanged` including the new value, + * `INVALIDATES` - emit `PropertiesChanged` listing the property as invalidated (without value), + * `CONST` / `FALSE` - do not emit a signal. + +The signal is only emitted after the setter completed successfully. The value is read back via the +property's getter (falling back to the value that was set if the property is write-only). + ### Using The Exported Object And finally making use of the exported interface in client code. From 9f84e0e1d05cc2a30233ba59760bb30859254b6f Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 19:43:35 +0200 Subject: [PATCH 24/38] Fixed BecomingMonitor handling --- .../org/freedesktop/dbus/bin/DBusDaemon.java | 94 ++++++++++++++++++- .../base/ConnectionMessageHandler.java | 50 ++++++++++ .../dbus/connections/impl/DBusConnection.java | 41 ++++++++ .../dbus/interfaces/DBusMonitorHandler.java | 25 +++++ .../dbus/interfaces/Monitoring.java | 4 +- .../freedesktop/dbus/test/MonitorTest.java | 64 +++++++++++++ src/site/markdown/using-signals.md | 19 ++++ 7 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBusMonitorHandler.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java index 50c901b25..2078572b5 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java @@ -14,6 +14,7 @@ import org.freedesktop.dbus.interfaces.DBus.NameOwnerChanged; import org.freedesktop.dbus.interfaces.FatalException; import org.freedesktop.dbus.interfaces.Introspectable; +import org.freedesktop.dbus.interfaces.Monitoring; import org.freedesktop.dbus.interfaces.Peer; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.matchrules.MatchRuleParser; @@ -141,6 +142,9 @@ public void run() { send(connectionStruct, messageFactory.createError(DBUS_BUSNAME, null, "org.freedesktop.DBus.Error.GeneralError", m.getSerial(), "s", "Sending message failed")); } + // deliver a copy of every message flowing through the bus to monitor connections + deliverToMonitors(m); + if (DBUS_BUSNAME.equals(m.getDestination())) { dbusServer.handleMessage(connectionStruct, pollFirst.first); } else { @@ -192,6 +196,9 @@ private void handleMatchRules(Message _msg, ConnectionStruct _currentConnection) } CON_LOOP: for (Entry cs : l.entrySet()) { + if (cs.getKey().monitor) { + continue; // monitors are served separately via deliverToMonitors + } for (DBusMatchRule rule : cs.getKey().rules) { if (rule.matches(_msg)) { LOGGER.debug("Cloning message for matchrule \"{}\" for connection {} (origin={})", @@ -210,6 +217,40 @@ private void handleMatchRules(Message _msg, ConnectionStruct _currentConnection) } } + /** + * Delivers a copy of the given message to all monitor connections whose monitor match rules match + * (an empty rule set matches everything). + * + * @param _msg the message flowing through the bus + */ + private void deliverToMonitors(Message _msg) { + Map l; + synchronized (conns) { + l = new HashMap<>(conns); + } + + for (ConnectionStruct cs : l.keySet()) { + if (!cs.monitor) { + continue; + } + if (cs.monitorRules.isEmpty() || monitorRulesMatch(cs, _msg)) { + LOGGER.trace("Delivering monitored message {} to monitor {}", _msg, cs.unique); + send(cs, _msg); + } + } + } + + private static boolean monitorRulesMatch(ConnectionStruct _cs, Message _msg) { + synchronized (_cs.monitorRules) { + for (DBusMatchRule rule : _cs.monitorRules) { + if (rule.matches(_msg)) { + return true; + } + } + } + return false; + } + private static void logMessage(String _logStr, Message _m, String _connUniqueId) { Object logMsg = _m; if (_m != null && Introspectable.class.getName().equals(_m.getInterface()) && !LOGGER.isTraceEnabled()) { @@ -418,9 +459,15 @@ public static class ConnectionStruct { private String unique; private Supplier threadSupplier; + /** Whether this connection became a monitor connection (via BecomeMonitor). */ + private boolean monitor; + /** Match rules restricting the monitored messages (empty = match all). */ + private final Set monitorRules; + ConnectionStruct(TransportConnection _c) { connection = _c; rules = Collections.synchronizedSet(new LinkedHashSet<>()); + monitorRules = Collections.synchronizedSet(new LinkedHashSet<>()); } @Override @@ -442,7 +489,7 @@ void updateThreadName() { } } - public class DBusServer implements DBus, Introspectable, Peer { + public class DBusServer implements DBus, Introspectable, Peer, Monitoring { private final String machineId; private ConnectionStruct connStruct; @@ -670,6 +717,16 @@ private void handleMessage(ConnectionStruct _connStruct, Message _msg) throws DB Object rv = null; MessageFactory messageFactory = _connStruct.connection.getMessageFactory(); + // BecomeMonitor takes an array argument; the reflective dispatch below matches on the runtime + // classes of the deserialized arguments (a D-Bus array deserializes to a List, not String[]), + // so it is handled explicitly here. + if ("BecomeMonitor".equals(_msg.getName())) { + this.connStruct = _connStruct; + BecomeMonitor(extractRuleStrings(args), new UInt32(0)); + send(_connStruct, messageFactory.createMethodReturn(DBUS_BUSNAME, (MethodCall) _msg, null), true); + return; + } + try { meth = DBusServer.class.getMethod(_msg.getName(), cs); try { @@ -699,6 +756,41 @@ private void handleMessage(ConnectionStruct _connStruct, Message _msg) throws DB } + private static String[] extractRuleStrings(Object[] _args) { + if (_args.length == 0 || _args[0] == null) { + return new String[0]; + } + Object first = _args[0]; + if (first instanceof String[] sa) { + return sa; + } else if (first instanceof Collection c) { + return c.stream().map(String::valueOf).toArray(String[]::new); + } else if (first instanceof Object[] oa) { + return Arrays.stream(oa).map(String::valueOf).toArray(String[]::new); + } + return new String[0]; + } + + @Override + public void BecomeMonitor(String[] _rule, UInt32 _flags) { + ConnectionStruct cs = connStruct; + if (cs == null) { + return; + } + cs.monitorRules.clear(); + for (String ruleStr : _rule) { + try { + cs.monitorRules.add(MatchRuleParser.convertMatchRule(ruleStr)); + } catch (RuntimeException _ex) { + LOGGER.warn("Ignoring invalid monitor match rule '{}'", ruleStr, _ex); + } + } + // a monitor connection loses its normal match rules and only receives monitored traffic + cs.rules.clear(); + cs.monitor = true; + LOGGER.debug("Connection {} became a monitor ({} rule(s))", cs.unique, cs.monitorRules.size()); + } + @Override public String getObjectPath() { return null; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index 5794fcd43..670541610 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -13,6 +13,7 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.CallbackHandler; +import org.freedesktop.dbus.interfaces.DBusMonitorHandler; import org.freedesktop.dbus.interfaces.DBusSigHandler; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.messages.*; @@ -34,10 +35,30 @@ */ public abstract sealed class ConnectionMessageHandler extends DBusBoundPropertyHandler permits AbstractConnection { + /** When set, this connection acts as a monitor and raw messages are delivered to this handler. */ + private volatile DBusMonitorHandler monitorHandler; + protected ConnectionMessageHandler(ConnectionConfig _conCfg, TransportConfig _transportConfig, ReceivingServiceConfig _rsCfg) throws DBusException { super(_conCfg, _transportConfig, _rsCfg); } + /** + * Sets (or clears with {@code null}) the monitor handler. When set, this connection is treated as a + * monitor connection: incoming messages are delivered to the handler instead of the normal dispatch. + * + * @param _monitorHandler monitor handler or {@code null} to leave monitor mode + */ + protected void setMonitorHandler(DBusMonitorHandler _monitorHandler) { + monitorHandler = _monitorHandler; + } + + /** + * @return true if this connection currently is a monitor connection + */ + protected boolean isMonitor() { + return monitorHandler != null; + } + @Override protected void handleException(Message _methodOrSignal, DBusExecutionException _exception) { try { @@ -241,6 +262,16 @@ public synchronized void run() { * @throws DBusException */ void handleMessage(Message _message) throws DBusException { + DBusMonitorHandler monitor = monitorHandler; + if (monitor != null && !isReplyToPendingCall(_message)) { + try { + monitor.handle(_message); + } catch (RuntimeException _ex) { + getLogger().warn("Monitor handler failed for message {}", _message, _ex); + } + return; + } + if (_message instanceof DBusSignal sig) { handleMessage(sig, true); } else if (_message instanceof MethodCall mc) { @@ -252,6 +283,25 @@ void handleMessage(Message _message) throws DBusException { } } + /** + * Checks whether the given message is a reply (return or error) to a call this connection is still + * awaiting. Used so a monitor connection can still complete its own {@code BecomeMonitor} call. + * + * @param _message message to check + * @return true if the message replies to a pending call of this connection + */ + private boolean isReplyToPendingCall(Message _message) { + long replySerial; + if (_message instanceof MethodReturn mr) { + replySerial = mr.getReplySerial(); + } else if (_message instanceof Error err) { + replySerial = err.getReplySerial(); + } else { + return false; + } + return getPendingCalls() != null && getPendingCalls().containsKey(replySerial); + } + private void handleMessage(final MethodCall _methodCall) throws DBusException { getLogger().debug("Handling incoming method call: {}", _methodCall); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java index c8faab04c..5d04c58f0 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java @@ -17,8 +17,10 @@ import org.freedesktop.dbus.exceptions.NotConnected; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.DBusMonitorHandler; import org.freedesktop.dbus.interfaces.DBusSigHandler; import org.freedesktop.dbus.interfaces.Introspectable; +import org.freedesktop.dbus.interfaces.Monitoring; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.matchrules.DBusMatchRuleBuilder; import org.freedesktop.dbus.messages.DBusSignal; @@ -297,6 +299,45 @@ public void requestBusName(String _busname) throws DBusException { doWithBusNames(bn -> bn.add(_busname)); } + /** + * Turns this connection into a monitor connection by calling + * {@code org.freedesktop.DBus.Monitoring.BecomeMonitor} on the bus. + *

+ * After this call, the connection receives copies of the messages flowing over the bus (as + * permitted by the given match rules) via the supplied {@link DBusMonitorHandler} instead of the + * connection's normal message handling. A monitor connection loses its bus names and its match + * rules and must not send messages anymore; use a dedicated (private) connection + * for monitoring. + *

+ *

+ * An empty (or {@code null}) rule list is a shorthand for matching all messages. Note that + * eavesdrop-style match rule keys are intentionally not supported by this library; {@code BecomeMonitor} + * is the specification-compliant replacement for eavesdropping. + *

+ * + * @param _rules match rules limiting the monitored messages, empty/{@code null} matches everything + * @param _handler callback receiving the monitored messages + * + * @throws DBusException when the BecomeMonitor call fails (e.g. insufficient privileges) + */ + public void becomeMonitor(List _rules, DBusMonitorHandler _handler) throws DBusException { + Objects.requireNonNull(_handler, "Monitor handler required"); + + String[] ruleStrings = _rules == null ? new String[0] + : _rules.stream().map(DBusMatchRule::toString).toArray(String[]::new); + + // activate monitor mode before the call; replies to our own pending calls (the BecomeMonitor + // reply itself) are still processed normally, everything else is routed to the handler + setMonitorHandler(_handler); + try { + Monitoring monitoring = getRemoteObject("org.freedesktop.DBus", "/org/freedesktop/DBus", Monitoring.class); + monitoring.BecomeMonitor(ruleStrings, new UInt32(0)); + } catch (RuntimeException _ex) { + setMonitorHandler(null); + throw _ex; + } + } + /** * Returns the unique name of this connection. * diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBusMonitorHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBusMonitorHandler.java new file mode 100644 index 000000000..f998559a5 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBusMonitorHandler.java @@ -0,0 +1,25 @@ +package org.freedesktop.dbus.interfaces; + +import org.freedesktop.dbus.messages.Message; + +/** + * Callback which receives the raw messages seen by a monitor connection. + *

+ * A connection is turned into a monitor connection via + * {@code DBusConnection.becomeMonitor(...)}. After that, it receives copies of the messages flowing + * over the bus (as permitted by the given match rules) instead of the connection's normal message + * handling. Each such message is delivered to this handler as a raw {@link Message}. + *

+ * + * @since 5.2.1 - 2026-07-18 + */ +@FunctionalInterface +public interface DBusMonitorHandler { + + /** + * Handle a message received on a monitor connection. + * + * @param _message the monitored message + */ + void handle(Message _message); +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Monitoring.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Monitoring.java index d97d792da..5f90d722d 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Monitoring.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Monitoring.java @@ -3,9 +3,9 @@ import org.freedesktop.dbus.annotations.DBusInterfaceName; import org.freedesktop.dbus.types.UInt32; -@DBusInterfaceName("org.freedesktop.DBus.Monitoring.BecomeMonitor") +@DBusInterfaceName("org.freedesktop.DBus.Monitoring") @SuppressWarnings({"checkstyle:methodname"}) -public interface Monitoring { +public interface Monitoring extends DBusInterface { /** * Converts the connection into a monitor connection which can be used as a * debugging/monitoring tool. Only a user who is privileged on this bus (by some implementation-specific definition) diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java new file mode 100644 index 000000000..b1d8806db --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java @@ -0,0 +1,64 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.messages.DBusSignal; +import org.freedesktop.dbus.messages.Message; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Verifies that a connection turned into a monitor via {@code becomeMonitor(...)} receives copies of + * the traffic flowing over the (embedded) bus. + */ +public class MonitorTest extends AbstractDBusBaseTest { + + @Test + @Timeout(value = 20, unit = TimeUnit.SECONDS) + void testMonitorReceivesBusTraffic() throws Exception { + BlockingQueue received = new LinkedBlockingQueue<>(); + CountDownLatch latch = new CountDownLatch(1); + + try (DBusConnection monitorConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + // empty rule list -> monitor all messages + monitorConn.becomeMonitor(List.of(), msg -> { + received.add(msg); + if ("PingSignal".equals(msg.getName())) { + latch.countDown(); + } + }); + + // generate traffic from another connection; the monitor must see a copy + serverconn.sendMessage(new MonitorTestSignals.PingSignal(getTestObjectPath(), "hello-monitor")); + + assertTrue(latch.await(15, TimeUnit.SECONDS), "monitor did not receive the emitted signal"); + + Message sig = received.stream() + .filter(m -> "PingSignal".equals(m.getName())) + .findFirst() + .orElseThrow(); + + assertEquals("org.freedesktop.dbus.test.MonitorTestSignals", sig.getInterface()); + assertEquals(getTestObjectPath(), sig.getPath()); + assertEquals("hello-monitor", sig.getParameters()[0]); + } + } + + @DBusInterfaceName("org.freedesktop.dbus.test.MonitorTestSignals") + public interface MonitorTestSignals extends DBusInterface { + class PingSignal extends DBusSignal { + public PingSignal(String _path, String _value) throws DBusException { + super(_path, _value); + } + } + } +} diff --git a/src/site/markdown/using-signals.md b/src/site/markdown/using-signals.md index c6a073664..33031c4b9 100644 --- a/src/site/markdown/using-signals.md +++ b/src/site/markdown/using-signals.md @@ -92,6 +92,25 @@ For more information on MatchRules see [DBus-Specification](https://dbus.freedes Please note: dbus-java does not support the eavesdrop option. Eavesdrop is deprecated according to specification, therefore there are no plans to add it. +### Monitoring the bus (`BecomeMonitor`) + +If you need to observe traffic that is not addressed to your connection, use the specification-compliant +monitor mechanism instead of eavesdropping. Calling `becomeMonitor(...)` on a `DBusConnection` turns it +into a *monitor connection* which receives copies of the messages flowing over the bus: + +```java +try (DBusConnection monitor = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + // empty rule list = monitor all messages; otherwise pass DBusMatchRule instances to filter + monitor.becomeMonitor(List.of(), msg -> + System.out.println("saw " + msg.getInterface() + "." + msg.getName() + " on " + msg.getPath())); + // ... keep the connection open while monitoring ... +} +``` + +A monitor connection loses its bus names and match rules and **must not send messages** anymore, so use a +dedicated (private) connection for it. Becoming a monitor usually requires elevated privileges on the bus. +Note that the callback receives raw `org.freedesktop.dbus.messages.Message` objects. + ## Signals and constructors A signal class can have multiple constructors. From 0664f2f0be45bd996145e0de3e28262545d5b562 Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 20:20:50 +0200 Subject: [PATCH 25/38] Improved BusAddress parsing --- .../dbus/connections/BusAddress.java | 110 +++++++++++++++++- .../dbus/connections/BusAddressTest.java | 46 ++++++++ 2 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/BusAddressTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java index f50b52f68..cc4e20c85 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java @@ -4,6 +4,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; @@ -12,7 +14,8 @@ * The address will define which transport to use. */ public class BusAddress { - private static final Logger LOGGER = LoggerFactory.getLogger(BusAddress.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BusAddress.class); + private static final char[] HEX = "0123456789ABCDEF".toCharArray(); private String type; private final Map parameters = new LinkedHashMap<>(); @@ -45,10 +48,43 @@ public static BusAddress of(BusAddress _address) { * @since 4.2.0 - 2022-07-18 */ public static BusAddress of(String _address) { + List all = parseAll(_address); + if (all.isEmpty()) { + throw new InvalidBusAddressException("Bus address is invalid: " + _address); + } + return all.getFirst(); + } + + /** + * Parses an address string which may contain multiple {@code ;}-separated addresses (as defined by + * the D-Bus specification) into a list of {@link BusAddress} objects, in the order given. + * + * @param _address address String, never null or empty + * + * @return list of BusAddress (never empty) + * @since 6.0.0 - 2026-07-18 + */ + public static List parseAll(String _address) { if (_address == null || _address.isEmpty()) { throw new InvalidBusAddressException("Bus address is blank"); } + List result = new ArrayList<>(); + for (String single : _address.split(";")) { + if (single.isBlank()) { + continue; + } + result.add(parseSingle(single)); + } + + if (result.isEmpty()) { + throw new InvalidBusAddressException("Bus address is invalid: " + _address); + } + + return result; + } + + private static BusAddress parseSingle(String _address) { BusAddress busAddress = new BusAddress(null); LOGGER.trace("Parsing bus address: {}", _address); @@ -65,10 +101,14 @@ public static BusAddress of(String _address) { LOGGER.trace("Transport type: {}", busAddress.type); - String[] ps = ss[1].split(","); - for (String p : ps) { - String[] kv = p.split("=", 2); - busAddress.addParameter(kv[0], kv[1]); + if (!ss[1].isEmpty()) { + for (String p : ss[1].split(",")) { + if (p.isEmpty()) { + continue; + } + String[] kv = p.split("=", 2); + busAddress.addParameter(kv[0], kv.length > 1 ? unescapeValue(kv[1]) : ""); + } } LOGGER.trace("Transport options: {}", busAddress.parameters); @@ -127,7 +167,65 @@ public String getGuid() { @Override public final String toString() { - return type + ":" + parameters.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(",")); + return type + ":" + parameters.entrySet().stream() + .map(e -> e.getKey() + "=" + escapeValue(e.getValue())) + .collect(Collectors.joining(",")); + } + + /** + * Decodes a {@code %HH}-escaped address parameter value (D-Bus address escaping) back to its raw + * value. Decoding is byte-based (UTF-8); a {@code %} not followed by two hex digits is left as-is. + * + * @param _value escaped value + * @return decoded value + */ + private static String unescapeValue(String _value) { + if (_value.indexOf('%') < 0) { + return _value; // nothing to unescape + } + + byte[] raw = _value.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream out = new ByteArrayOutputStream(raw.length); + for (int i = 0; i < raw.length; i++) { + byte b = raw[i]; + if (b == '%' && i + 2 < raw.length && isHex(raw[i + 1]) && isHex(raw[i + 2])) { + out.write((Character.digit(raw[i + 1], 16) << 4) | Character.digit(raw[i + 2], 16)); + i += 2; + } else { + out.write(b); + } + } + return out.toString(StandardCharsets.UTF_8); + } + + /** + * Escapes an address parameter value according to the D-Bus address escaping rules: every byte which + * is not in the optionally-escaped set {@code [-0-9A-Za-z_/.*]} is replaced by {@code %HH}. + * + * @param _value raw value + * @return escaped value + */ + private static String escapeValue(String _value) { + byte[] raw = _value.getBytes(StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(raw.length); + for (byte b : raw) { + int c = b & 0xFF; + if (isOptionallyEscaped(c)) { + sb.append((char) c); + } else { + sb.append('%').append(HEX[(c >> 4) & 0xF]).append(HEX[c & 0xF]); + } + } + return sb.toString(); + } + + private static boolean isOptionallyEscaped(int _c) { + return _c == '-' || _c == '_' || _c == '/' || _c == '.' || _c == '*' + || _c >= '0' && _c <= '9' || _c >= 'A' && _c <= 'Z' || _c >= 'a' && _c <= 'z'; + } + + private static boolean isHex(byte _b) { + return _b >= '0' && _b <= '9' || _b >= 'a' && _b <= 'f' || _b >= 'A' && _b <= 'F'; } /** diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/BusAddressTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/BusAddressTest.java new file mode 100644 index 000000000..c6ab53d0e --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/BusAddressTest.java @@ -0,0 +1,46 @@ +package org.freedesktop.dbus.connections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +class BusAddressTest { + + @Test + void testUnescapesParameterValues() { + BusAddress addr = BusAddress.of("unix:path=%2Ftmp%2Fdbus-test"); + assertEquals("/tmp/dbus-test", addr.getParameterValue("path")); + } + + @Test + void testEscapingRoundTrip() { + BusAddress addr = BusAddress.of("tcp:host=my%20host,port=1234"); + assertEquals("my host", addr.getParameterValue("host")); + + // toString must re-escape the value so it can be parsed back into the same address + String str = addr.toString(); + assertTrue(str.contains("host=my%20host"), "value must be re-escaped in toString: " + str); + assertEquals("my host", BusAddress.of(str).getParameterValue("host")); + } + + @Test + void testOfParsesOnlyFirstAddressOfList() { + BusAddress addr = BusAddress.of("unix:path=/first;tcp:host=example,port=1"); + assertTrue(addr.isBusType("unix")); + assertEquals("/first", addr.getParameterValue("path")); // no ';tcp:...' leaking into the value + } + + @Test + void testParseAllReturnsEveryAddress() { + List all = BusAddress.parseAll("unix:path=/a;tcp:host=x,port=1"); + assertEquals(2, all.size()); + assertTrue(all.get(0).isBusType("unix")); + assertEquals("/a", all.get(0).getParameterValue("path")); + assertTrue(all.get(1).isBusType("tcp")); + assertEquals("x", all.get(1).getParameterValue("host")); + assertEquals("1", all.get(1).getParameterValue("port")); + } +} From d983cf479d807c78f6becccddd1869a347b3aa1c Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 21:12:30 +0200 Subject: [PATCH 26/38] Added default implementation of DBusObjectManager --- UPGRADE_TO_6x.md | 20 ++ .../dbus/connections/AbstractConnection.java | 21 ++ .../base/ConnectionMessageHandler.java | 226 ++++++++++++++++++ .../base/DBusBoundPropertyHandler.java | 2 +- .../impl/BaseConnectionBuilder.java | 20 ++ .../connections/impl/ConnectionConfig.java | 9 + .../dbus/connections/impl/DBusConnection.java | 22 ++ .../connections/impl/DBusObjectManager.java | 45 ++++ .../freedesktop/dbus/utils/DBusObjects.java | 17 ++ .../dbus/test/ObjectManagerTest.java | 136 +++++++++++ 10 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusObjectManager.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java diff --git a/UPGRADE_TO_6x.md b/UPGRADE_TO_6x.md index bb9537813..7349aaf8d 100644 --- a/UPGRADE_TO_6x.md +++ b/UPGRADE_TO_6x.md @@ -10,3 +10,23 @@ This allows you to use shorter definitions when creating or generating code. Ins To instruct the InterfaceCodeGenerator to create `Struct` based return values instead of `Tuple`s, use the new `--disable-tuples` option. Please be aware, that the created code will only work with dbus-java 6.x and will fail during runtime when used with older versions! + +#### Behaviour change: automatic `ObjectManager` handling + +Starting with dbus-java 6.x, exported objects implementing `org.freedesktop.DBus.ObjectManager` are handled +automatically by default: dbus-java answers `GetManagedObjects` itself (by enumerating the exported sub-tree +and collecting each object's properties) and automatically emits `InterfacesAdded`/`InterfacesRemoved` when +objects below an `ObjectManager` are exported/unexported. + +If you already provide your own server-side `ObjectManager` implementation and want to keep full manual +control (your own `GetManagedObjects` and your own signal emission), build the connection with +`withManualObjectManager(true)`: + +```java +DBusConnection conn = DBusConnectionBuilder.forSessionBus() + .withManualObjectManager(true) + .build(); +``` + +If you do not implement `ObjectManager` yourself, you can now export a ready-to-use one via +`connection.exportObjectManager("/your/path")` without writing any class. diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java index 7a6e09ac3..f09cfccaf 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/AbstractConnection.java @@ -246,6 +246,27 @@ public void exportObject(String _objectPath, DBusInterface _object) throws DBusE getObjectTree().add(_objectPath, eo, eo.getIntrospectiondata()); } }); + + // automatically announce the new object to an ObjectManager above it (unless manual handling) + emitInterfacesAdded(_objectPath); + } + + @Override + public void unExportObject(String _objectpath) { + // collect the object's interfaces before removal so an InterfacesRemoved signal can be emitted + List interfaceNames = null; + if (!getConnectionConfig().isManualObjectManager()) { + ExportedObject eo = doWithExportedObjectsAndReturn(RuntimeException.class, eos -> eos.get(_objectpath)); + if (eo != null) { + interfaceNames = collectInterfaceNames(eo); + } + } + + super.unExportObject(_objectpath); + + if (interfaceNames != null) { + emitInterfacesRemoved(_objectpath, interfaceNames); + } } /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index 670541610..b4ceb9071 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -2,8 +2,10 @@ import org.freedesktop.dbus.DBusAsyncReply; import org.freedesktop.dbus.DBusCallInfo; +import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.MethodTuple; import org.freedesktop.dbus.RemoteInvocationHandler; +import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.connections.AbstractConnection; import org.freedesktop.dbus.connections.config.ReceivingServiceConfig; import org.freedesktop.dbus.connections.config.TransportConfig; @@ -13,15 +15,26 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.CallbackHandler; +import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.DBusMonitorHandler; import org.freedesktop.dbus.interfaces.DBusSigHandler; +import org.freedesktop.dbus.interfaces.Introspectable; +import org.freedesktop.dbus.interfaces.ObjectManager; +import org.freedesktop.dbus.interfaces.Peer; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.messages.*; import org.freedesktop.dbus.messages.Error; +import org.freedesktop.dbus.propertyref.PropertyRef; +import org.freedesktop.dbus.types.Variant; +import org.freedesktop.dbus.utils.DBusNamingUtil; +import org.freedesktop.dbus.utils.DBusObjects; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Queue; @@ -35,6 +48,8 @@ */ public abstract sealed class ConnectionMessageHandler extends DBusBoundPropertyHandler permits AbstractConnection { + private static final Method GET_MANAGED_OBJECTS_METHOD = getManagedObjectsMethod(); + /** When set, this connection acts as a monitor and raw messages are delivered to this handler. */ private volatile DBusMonitorHandler monitorHandler; @@ -354,6 +369,15 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { } } + // automatic ObjectManager handling (unless the connection is configured for manual handling) + if (!getConnectionConfig().isManualObjectManager() + && "GetManagedObjects".equals(_methodCall.getName()) + && (_methodCall.getInterface() == null || "org.freedesktop.DBus.ObjectManager".equals(_methodCall.getInterface())) + && exportObject.getObject().get() instanceof ObjectManager) { + handleGetManagedObjects(_methodCall); + return; + } + Object[] params = _methodCall.getParameters(); switch (handleDBusBoundProperties(exportObject, _methodCall, params)) { case HANDLED: @@ -387,4 +411,206 @@ private void handleMessage(final MethodCall _methodCall) throws DBusException { queueInvokeMethod(_methodCall, meth, o); } + private static Method getManagedObjectsMethod() { + try { + return ObjectManager.class.getMethod("GetManagedObjects"); + } catch (NoSuchMethodException _ex) { + throw new IllegalStateException("ObjectManager.GetManagedObjects method not found", _ex); + } + } + + /** + * Answers an {@code org.freedesktop.DBus.ObjectManager.GetManagedObjects} call automatically by + * enumerating the exported sub-tree below the ObjectManager and collecting each object's interfaces + * and properties. + * + * @param _methodCall the GetManagedObjects call + */ + private void handleGetManagedObjects(final MethodCall _methodCall) { + getReceivingService().execMethodCallHandler(() -> { + try { + Map>>> managed = buildManagedObjects(_methodCall.getPath()); + invokedMethodReply(_methodCall, GET_MANAGED_OBJECTS_METHOD, managed); + } catch (DBusExecutionException _ex) { + getLogger().debug("Failed to answer GetManagedObjects", _ex); + handleException(_methodCall, _ex); + } catch (Exception _ex) { + getLogger().debug("Failed to build managed objects for {}", _methodCall, _ex); + handleException(_methodCall, + new DBusExecutionException("Error building managed objects: " + _ex.getMessage(), _ex)); + } + }); + } + + /** + * Builds the {@code GetManagedObjects} response for the sub-tree below the given root path: every + * exported object which is a descendant of {@code _rootPath}, mapped to its interfaces and their + * properties (as {@code Properties.GetAll()} would return them). + * + * @param _rootPath object path of the ObjectManager (root of the sub-tree) + * + * @return map of object path to interface-to-properties map + */ + public Map>>> buildManagedObjects(String _rootPath) { + return doWithExportedObjectsAndReturn(RuntimeException.class, eos -> { + Map>>> result = new LinkedHashMap<>(); + for (Entry e : eos.entrySet()) { + String path = e.getKey(); + if (path == null || path.equals(_rootPath)) { + continue; + } + boolean descendant = "/".equals(_rootPath) ? !"/".equals(path) : path.startsWith(_rootPath + "/"); + if (!descendant) { + continue; + } + result.put(new DBusPath(path), collectManagedInterfaces(e.getValue())); + } + return result; + }); + } + + /** + * Collects the (non-standard) interfaces of an exported object together with their current property + * values. Bound properties ({@code @DBusBoundProperty}) are read via their getters; objects + * implementing {@link Properties} directly are queried via {@code GetAll}. + * + * @param _eo exported object + * + * @return map of interface name to property map + */ + protected Map>> collectManagedInterfaces(ExportedObject _eo) { + Map>> byInterface = new LinkedHashMap<>(); + DBusInterface obj = _eo.getObject().get(); + if (obj == null) { + return byInterface; + } + + for (Class iface : _eo.getImplementedInterfaces()) { + if (DBusObjects.isStandardInterface(iface)) { + continue; + } + String ifaceName = DBusNamingUtil.getInterfaceName(iface); + Map> props = new LinkedHashMap<>(); + + // read bound properties declared on this interface + for (Entry pe : _eo.getPropertyMethods().entrySet()) { + Method getter = pe.getValue(); + if (pe.getKey().getAccess() == Access.READ && getter.getDeclaringClass() == iface) { + try { + Object value = getter.invoke(obj); + if (value != null) { + props.put(pe.getKey().getName(), toVariant(value, getter.getGenericReturnType())); + } + } catch (Exception _ex) { + getLogger().debug("Failed to read bound property {} for managed objects", pe.getKey().getName(), _ex); + } + } + } + + // objects implementing the Properties interface directly + if (obj instanceof Properties p) { + try { + Map> all = p.GetAll(ifaceName); + if (all != null) { + props.putAll(all); + } + } catch (RuntimeException _ex) { + getLogger().debug("GetAll failed for interface {} while building managed objects", ifaceName, _ex); + } + } + + byInterface.put(ifaceName, props); + } + return byInterface; + } + + /** + * Returns the object path of the closest exported {@link ObjectManager} which is an ancestor of the + * given path, or {@code null} if none exists. + * + * @param _path object path to find a managing ObjectManager for + * + * @return ObjectManager object path or {@code null} + */ + protected String findObjectManagerAncestor(String _path) { + return doWithExportedObjectsAndReturn(RuntimeException.class, eos -> { + String best = null; + for (Entry e : eos.entrySet()) { + String mgrPath = e.getKey(); + if (mgrPath == null || !(e.getValue().getObject().get() instanceof ObjectManager)) { + continue; + } + boolean ancestor = "/".equals(mgrPath) ? !"/".equals(_path) : _path.startsWith(mgrPath + "/"); + if (ancestor && (best == null || mgrPath.length() > best.length())) { + best = mgrPath; + } + } + return best; + }); + } + + /** + * Emits an {@code InterfacesAdded} signal for the given newly exported object, if automatic + * ObjectManager handling is enabled and the object lives below an exported ObjectManager. + * + * @param _objectPath path of the exported object + */ + protected void emitInterfacesAdded(String _objectPath) { + if (getConnectionConfig().isManualObjectManager()) { + return; + } + String mgrPath = findObjectManagerAncestor(_objectPath); + if (mgrPath == null) { + return; + } + ExportedObject eo = doWithExportedObjectsAndReturn(RuntimeException.class, eos -> eos.get(_objectPath)); + if (eo == null) { + return; + } + try { + sendMessage(new ObjectManager.InterfacesAdded(mgrPath, new DBusPath(_objectPath), collectManagedInterfaces(eo))); + } catch (DBusException _ex) { + getLogger().warn("Failed to emit InterfacesAdded for {}", _objectPath, _ex); + } + } + + /** + * Emits an {@code InterfacesRemoved} signal for an unexported object, if automatic ObjectManager + * handling is enabled and the object lived below an exported ObjectManager. + * + * @param _objectPath path of the (now unexported) object + * @param _interfaceNames the interface names the object provided + */ + protected void emitInterfacesRemoved(String _objectPath, List _interfaceNames) { + if (getConnectionConfig().isManualObjectManager() || _interfaceNames.isEmpty()) { + return; + } + String mgrPath = findObjectManagerAncestor(_objectPath); + if (mgrPath == null) { + return; + } + try { + sendMessage(new ObjectManager.InterfacesRemoved(mgrPath, new DBusPath(_objectPath), _interfaceNames)); + } catch (DBusException _ex) { + getLogger().warn("Failed to emit InterfacesRemoved for {}", _objectPath, _ex); + } + } + + /** + * Collects the non-standard DBus interface names an exported object provides. + * + * @param _eo exported object + * + * @return list of interface names + */ + protected List collectInterfaceNames(ExportedObject _eo) { + List names = new ArrayList<>(); + for (Class iface : _eo.getImplementedInterfaces()) { + if (!DBusObjects.isStandardInterface(iface)) { + names.add(DBusNamingUtil.getInterfaceName(iface)); + } + } + return names; + } + } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java index e2eed6607..fba744fdc 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/DBusBoundPropertyHandler.java @@ -378,7 +378,7 @@ private EmitChangeSignal resolveEmitChangeSignal(Method _setter) { * * @throws DBusException when the DBus type cannot be determined */ - private Variant toVariant(Object _value, Type _type) throws DBusException { + protected Variant toVariant(Object _value, Type _type) throws DBusException { if (_value.getClass().isArray() || _value instanceof Collection || _value instanceof Map) { String signature = String.join("", Marshalling.getDBusType(_type)); return new Variant<>(_value, signature); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java index 98fc2dba3..ccb5aaca3 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/BaseConnectionBuilder.java @@ -187,6 +187,26 @@ public R withAutoEmitPropertiesChanged(boolean _autoEmit) { return self(); } + /** + * Controls whether {@code org.freedesktop.DBus.ObjectManager} is handled automatically for exported + * objects implementing that interface. + *

+ * By default (false), dbus-java answers {@code GetManagedObjects} itself (by enumerating the exported + * sub-tree and collecting the properties of each object) and automatically emits {@code InterfacesAdded} + * and {@code InterfacesRemoved} when objects below an ObjectManager are exported/unexported. + *

+ *

+ * Set this to true to take full manual control: the exported object's own {@code GetManagedObjects} + * implementation is used and the application is responsible for emitting the signals. + *

+ * @param _manual true to disable the automatic ObjectManager handling + * @return this + */ + public R withManualObjectManager(boolean _manual) { + connectionConfig.setManualObjectManager(_manual); + return self(); + } + public abstract C build() throws DBusException; /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java index 0ce2ecc5c..6e9189a8b 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/ConnectionConfig.java @@ -11,6 +11,7 @@ public class ConnectionConfig { private IDisconnectCallback disconnectCallback; private Consumer unknownSignalHandler; private boolean autoEmitPropertiesChanged; + private boolean manualObjectManager; public boolean isExportWeakReferences() { return exportWeakReferences; @@ -52,4 +53,12 @@ public void setAutoEmitPropertiesChanged(boolean _autoEmitPropertiesChanged) { autoEmitPropertiesChanged = _autoEmitPropertiesChanged; } + public boolean isManualObjectManager() { + return manualObjectManager; + } + + public void setManualObjectManager(boolean _manualObjectManager) { + manualObjectManager = _manualObjectManager; + } + } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java index 5d04c58f0..f3e00b440 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnection.java @@ -338,6 +338,28 @@ public void becomeMonitor(List _rules, DBusMonitorHandler _handle } } + /** + * Exports a ready-to-use {@code org.freedesktop.DBus.ObjectManager} at the given object path. + *

+ * The manager answers {@code GetManagedObjects} automatically (by enumerating the exported objects + * below {@code _path}) and, together with the automatic ObjectManager handling, emits + * {@code InterfacesAdded}/{@code InterfacesRemoved} when objects below it are exported/unexported. + * You do not need to implement {@link org.freedesktop.dbus.interfaces.ObjectManager} yourself. + *

+ *

+ * Has no automatic effect if the connection was configured with + * {@code withManualObjectManager(true)} - in that case the exported manager behaves like any other + * exported object. + *

+ * + * @param _path object path for the ObjectManager (root of the managed sub-tree) + * + * @throws DBusException if the object path is already in use or invalid + */ + public void exportObjectManager(String _path) throws DBusException { + exportObject(_path, new DBusObjectManager(_path)); + } + /** * Returns the unique name of this connection. * diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusObjectManager.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusObjectManager.java new file mode 100644 index 000000000..d11b5c1be --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusObjectManager.java @@ -0,0 +1,45 @@ +package org.freedesktop.dbus.connections.impl; + +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.interfaces.ObjectManager; +import org.freedesktop.dbus.types.Variant; + +import java.util.Map; + +/** + * A ready-to-use {@link ObjectManager} implementation exported by + * {@link DBusConnection#exportObjectManager(String)}. + *

+ * Its {@code GetManagedObjects} is answered automatically by the connection (by enumerating the exported + * sub-tree), so the implementation here is just a placeholder. It exists to register the ObjectManager at + * a given object path without requiring the application to write its own class. + *

+ * + * @since 6.0.0 - 2026-07-18 + */ +public class DBusObjectManager implements ObjectManager { + + private final String objectPath; + + public DBusObjectManager(String _objectPath) { + objectPath = _objectPath; + } + + /** + * Placeholder - the connection answers GetManagedObjects automatically by enumerating the sub-tree. + */ + @Override + public Map>>> GetManagedObjects() { + return Map.of(); + } + + @Override + public String getObjectPath() { + return objectPath; + } + + @Override + public boolean isRemote() { + return false; + } +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusObjects.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusObjects.java index 395be793a..2d58b8d50 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusObjects.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/DBusObjects.java @@ -3,6 +3,10 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.exceptions.*; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.Introspectable; +import org.freedesktop.dbus.interfaces.ObjectManager; +import org.freedesktop.dbus.interfaces.Peer; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.DBusSignal; import java.lang.reflect.Modifier; @@ -435,4 +439,17 @@ public static void ensurePublicInterfaces(Object _object) throws InvalidInterfac } } + + /** + * Checks if the given interface is a standard interface. + * + * @param _iface interface to check + * @return true if the given interface is a standard interface + */ + public static boolean isStandardInterface(Class _iface) { + return _iface == DBusInterface.class || _iface == Properties.class + || _iface == Introspectable.class || _iface == Peer.class + || _iface == ObjectManager.class; + } + } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java new file mode 100644 index 000000000..1226549ef --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java @@ -0,0 +1,136 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.annotations.DBusBoundProperty; +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.ObjectManager; +import org.freedesktop.dbus.types.Variant; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class ObjectManagerTest extends AbstractDBusBaseTest { + + @Test + void testAutoGetManagedObjects() throws Exception { + serverconn.exportObjectManager("/com/acme"); + serverconn.exportObject(new DeviceImpl("/com/acme/dev1", "Device One")); + + ObjectManager mgr = clientconn.getRemoteObject(getTestBusName(), "/com/acme", ObjectManager.class); + Map>>> managed = mgr.GetManagedObjects(); + + DBusPath devPath = new DBusPath("/com/acme/dev1"); + assertTrue(managed.containsKey(devPath), "managed objects must contain the child, got: " + managed.keySet()); + Map>> ifaces = managed.get(devPath); + assertTrue(ifaces.containsKey("com.acme.Device1"), "interface missing, got: " + ifaces.keySet()); + assertEquals("Device One", ifaces.get("com.acme.Device1").get("Name").getValue()); + } + + @Test + @Timeout(value = 20, unit = TimeUnit.SECONDS) + void testAutoInterfacesAddedAndRemoved() throws Exception { + serverconn.exportObjectManager("/com/acme2"); + + CountDownLatch addedLatch = new CountDownLatch(1); + AtomicReference addedRef = new AtomicReference<>(); + clientconn.addSigHandler(ObjectManager.InterfacesAdded.class, s -> { + if ("/com/acme2/dev1".equals(s.getSignalSource().getPath())) { + addedRef.set(s); + addedLatch.countDown(); + } + }); + + serverconn.exportObject(new DeviceImpl("/com/acme2/dev1", "Dev")); + + assertTrue(addedLatch.await(15, TimeUnit.SECONDS), "InterfacesAdded not received"); + assertTrue(addedRef.get().getInterfaces().containsKey("com.acme.Device1"), + "added signal must list the interface, got: " + addedRef.get().getInterfaces().keySet()); + + CountDownLatch removedLatch = new CountDownLatch(1); + AtomicReference removedRef = new AtomicReference<>(); + clientconn.addSigHandler(ObjectManager.InterfacesRemoved.class, s -> { + if ("/com/acme2/dev1".equals(s.getSignalSource().getPath())) { + removedRef.set(s); + removedLatch.countDown(); + } + }); + + serverconn.unExportObject("/com/acme2/dev1"); + + assertTrue(removedLatch.await(15, TimeUnit.SECONDS), "InterfacesRemoved not received"); + assertTrue(removedRef.get().getInterfaces().contains("com.acme.Device1"), + "removed signal must list the interface, got: " + removedRef.get().getInterfaces()); + } + + @Test + void testManualObjectManagerUsesOwnImplementation() throws Exception { + try (DBusConnection manualConn = DBusConnectionBuilder.forSessionBus().withShared(false) + .withManualObjectManager(true).build()) { + manualConn.requestBusName("com.acme.manual"); + manualConn.exportObject(new CustomObjectManager("/mgr")); + + try (DBusConnection reader = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + ObjectManager mgr = reader.getRemoteObject("com.acme.manual", "/mgr", ObjectManager.class); + Map>>> managed = mgr.GetManagedObjects(); + + // the custom implementation returns a sentinel entry; the library must NOT intercept it + assertTrue(managed.containsKey(new DBusPath("/custom/sentinel")), + "manual ObjectManager implementation must be used, got: " + managed.keySet()); + } + } + } + + @DBusInterfaceName("com.acme.Device1") + public interface Device extends DBusInterface { + @DBusBoundProperty(access = Access.READ, name = "Name") + String getName(); + } + + public static class DeviceImpl implements Device { + private final String path; + private final String name; + + public DeviceImpl(String _path, String _name) { + path = _path; + name = _name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getObjectPath() { + return path; + } + } + + public static class CustomObjectManager implements ObjectManager { + private final String path; + + public CustomObjectManager(String _path) { + path = _path; + } + + @Override + public Map>>> GetManagedObjects() { + return Map.of(new DBusPath("/custom/sentinel"), Map.of("com.acme.Sentinel", Map.of())); + } + + @Override + public String getObjectPath() { + return path; + } + } +} From 145097151610bac21ddb0db7c346c81354b43a5f Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 21:41:38 +0200 Subject: [PATCH 27/38] Fixed: file descriptors could be read beyond limits Added safe guard for nested container and variants --- .../freedesktop/dbus/messages/Message.java | 38 ++++++++-- .../dbus/messages/MessageTest.java | 76 +++++++++++++++++++ 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java index 4ca32515c..2d6c4a5ef 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/messages/Message.java @@ -41,11 +41,14 @@ public class Message { public static final int MAXIMUM_MESSAGE_LENGTH = MAXIMUM_ARRAY_LENGTH * 2; public static final int MAXIMUM_NUM_UNIX_FDS = MAXIMUM_MESSAGE_LENGTH / 4; + /** Maximum container nesting depth honoured while demarshalling values (guards against deeply nested variants). */ + public static final int MAXIMUM_EXTRACT_DEPTH = 64; + /** The current protocol major version. */ public static final byte PROTOCOL = 1; /** Default extraction options. */ - private static final ExtractOptions DEFAULT_OPTIONS = new ExtractOptions(false, List.of()); + private static final ExtractOptions DEFAULT_OPTIONS = new ExtractOptions(false, List.of(), 0); /** Position of data offset in int array. */ private static final int OFFSET_DATA = 1; @@ -196,6 +199,18 @@ void populate(byte[] _msg, byte[] _headers, byte[] _body, List _ } this.headers[idx] = objArr[1]; } + + // validate the declared number of unix file descriptors against what was actually received + if (this.headers[HeaderField.UNIX_FDS] instanceof UInt32 declaredFds) { + long declared = declaredFds.longValue(); + if (declared > MAXIMUM_NUM_UNIX_FDS) { + throw new MarshallingException("Message declares too many unix file descriptors: " + declared); + } + if (declared != filedescriptors.size()) { + throw new MarshallingException("Message declares " + declared + + " unix file descriptors but " + filedescriptors.size() + " were received"); + } + } } protected Object[] getHeader() { @@ -836,6 +851,10 @@ private Object readHeaderVariants(byte[] _signatureBuf, byte[] _dataBuf, int[] _ private Object extractOne(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, ExtractOptions _options) throws DBusException { + if (_options.depth() > MAXIMUM_EXTRACT_DEPTH) { + throw new MarshallingException("Maximum container nesting depth (" + MAXIMUM_EXTRACT_DEPTH + ") exceeded"); + } + logger.trace("Extracting type: {} from offset {}", (char) _signatureBuf[_offsets[OFFSET_SIG]], _offsets[OFFSET_DATA]); @@ -924,8 +943,13 @@ private Object extractOne(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, }); break; case FILEDESCRIPTOR: - rv = filedescriptors.get((int) demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4)); + int fdIndex = (int) demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4); _offsets[OFFSET_DATA] += 4; + if (fdIndex < 0 || fdIndex >= filedescriptors.size()) { + throw new MarshallingException("File descriptor index " + fdIndex + + " out of bounds (received " + filedescriptors.size() + " file descriptors)"); + } + rv = filedescriptors.get(fdIndex); break; case STRING: int length = validateLengthLimit(demarshallint(_dataBuf, _offsets[OFFSET_DATA], 4), _offsets[OFFSET_DATA] + 4, _dataBuf.length); @@ -1075,7 +1099,8 @@ private Object extractVariant(byte[] _dataBuf, int[] _offsets, ExtractOptions _o }; String sig = (String) extract(SIGNATURE_STRING, _dataBuf, newofs, _options)[0]; newofs[OFFSET_SIG] = 0; - rv = _variantFactory.apply(sig, extract(sig, _dataBuf, newofs, _options)[0]); + // extract the variant content one nesting level deeper so nested variants are depth-limited + rv = _variantFactory.apply(sig, extract(sig, _dataBuf, newofs, ExtractOptions.copyWithContainedFlag(_options, true))[0]); _offsets[OFFSET_DATA] = newofs[OFFSET_DATA]; return rv; @@ -1382,7 +1407,7 @@ private Object[] extractArgs(List _constructorArgs) throws DBusException if (_constructorArgs != null && !_constructorArgs.isEmpty()) { List dataType = new ArrayList<>(); Marshalling.getJavaType(getSig(), dataType, -1); - options = new ExtractOptions(DEFAULT_OPTIONS.contained(), usesPrimitives(_constructorArgs, dataType)); + options = new ExtractOptions(DEFAULT_OPTIONS.contained(), usesPrimitives(_constructorArgs, dataType), 0); } if (sig != null && body != null && body.length != 0) { @@ -1763,11 +1788,12 @@ Object extractOne(byte[] _signatureBuf, byte[] _dataBuf, int[] _offsets, Extract */ record ExtractOptions( boolean contained, - List arrayConvert + List arrayConvert, + int depth ) { static ExtractOptions copyWithContainedFlag(ExtractOptions _toCopy, boolean _containedFlag) { - return new ExtractOptions(_containedFlag, _toCopy.arrayConvert()); + return new ExtractOptions(_containedFlag, _toCopy.arrayConvert(), _toCopy.depth() + 1); } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java index 69053a06a..4d5054d92 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java @@ -97,6 +97,82 @@ void testExtractArrayRejectsOversizedLength() { assertThrows(DBusException.class, m::getParameters); } + @Test + void testExtractRejectsDeeplyNestedVariants() { + int nesting = 200; // far beyond Message.MAXIMUM_EXTRACT_DEPTH (64) + + // body: `nesting` nested variants ("v" in "v" in ...) ending in a single byte value + byte[] body = new byte[nesting * 3 + 4]; + int i = 0; + for (int d = 0; d < nesting; d++) { + body[i++] = 1; // signature length + body[i++] = 'v'; // variant type code + body[i++] = 0; // nul terminator + } + body[i++] = 1; // signature length + body[i++] = 'y'; // byte type code (terminal) + body[i++] = 0; // nul terminator + body[i] = 0x42; // the byte value + + byte[] msg = {108, 1, 0, 1, + (byte) body.length, (byte) (body.length >>> 8), (byte) (body.length >>> 16), (byte) (body.length >>> 24), + 1, 0, 0, 0}; + byte[] headers = headerWithSignature((byte) 'v'); + + Message m = new Message(); + assertDoesNotThrow(() -> m.populate(msg, headers, body, null)); + DBusException ex = assertThrows(DBusException.class, m::getParameters); + assertTrue(ex.getMessage() != null && ex.getMessage().contains("nesting depth"), + "expected nesting depth error, got: " + ex.getMessage()); + } + + @Test + void testExtractFileDescriptorRejectsOutOfBoundsIndex() { + // signature "h": body is a 4-byte fd index (0), but no file descriptors were received + byte[] msg = {108, 1, 0, 1, 4, 0, 0, 0, 1, 0, 0, 0}; + byte[] headers = headerWithSignature((byte) 'h'); + byte[] body = {0, 0, 0, 0}; + + Message m = new Message(); + assertDoesNotThrow(() -> m.populate(msg, headers, body, null)); + DBusException ex = assertThrows(DBusException.class, m::getParameters); + assertTrue(ex.getMessage() != null && ex.getMessage().contains("out of bounds"), + "expected out-of-bounds error, got: " + ex.getMessage()); + } + + @Test + void testPopulateRejectsUnixFdCountMismatch() { + // header declares 2 unix fds (UNIX_FDS field 9), but none are actually received + byte[] msg = {108, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0}; + byte[] headers = { + 8, 0, 0, 0, // header array length (8 bytes of elements) + 0, 0, 0, 0, // padding to 8-align the first struct + 9, 1, 117, 0, // field UNIX_FDS(9), variant signature "u" + 2, 0, 0, 0 // UInt32 value = 2 + }; + byte[] body = {}; + + Message m = new Message(); + DBusException ex = assertThrows(DBusException.class, () -> m.populate(msg, headers, body, null)); + assertTrue(ex.getMessage() != null && ex.getMessage().contains("unix file descriptors"), + "expected fd count mismatch error, got: " + ex.getMessage()); + } + + /** + * Returns a valid message header (as used by {@link #testReadMessageHeader()}) whose SIGNATURE field + * value byte is replaced by the given type code. + */ + private static byte[] headerWithSignature(byte _sigChar) { + byte[] headers = { + 61, 0, 0, 0, 0, 0, 0, 0, 6, 1, 115, 0, 5, 0, 0, 0, 58, 49, 46, 50, 48, 0, 0, 0, 5, 1, + 117, 0, 1, 0, 0, 0, 8, 1, 103, 0, 1, 115, 0, 0, 7, 1, 115, 0, 20, 0, 0, 0, 111, 114, + 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 68, 66, 117, 115, + 0, 0, 0, 0 + }; + headers[37] = _sigChar; // SIGNATURE field value ('s' -> given type code) + return headers; + } + static Stream parameterSource() { return Stream.of( new ParameterData("Complex constructor", List.of(new Type[] {long.class, String.class, byte[].class, String.class, Map.class}, new Type[] {String.class}), From 35a990478680a81cc9a4b24d694ebf51436cc583 Mon Sep 17 00:00:00 2001 From: David M Date: Sat, 18 Jul 2026 22:18:12 +0200 Subject: [PATCH 28/38] Added debug output options to EmbeddedDbusDaemon --- .../org/freedesktop/dbus/bin/DBusDaemon.java | 114 +++++++++++++++++- .../bin/DebuggableEmbeddedDBusDaemon.java | 33 +++++ .../dbus/bin/EmbeddedDBusDaemon.java | 15 ++- .../base/ConnectionMessageHandler.java | 2 - .../freedesktop/dbus/interfaces/Debug.java | 60 +++++++++ .../freedesktop/dbus/bin/DebugStatsTest.java | 84 +++++++++++++ .../dbus/test/ObjectManagerTest.java | 2 - 7 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DebuggableEmbeddedDBusDaemon.java create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Debug.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java index 2078572b5..86083a00f 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java @@ -8,10 +8,13 @@ import org.freedesktop.dbus.connections.transports.TransportConnection; import org.freedesktop.dbus.errors.AccessDenied; import org.freedesktop.dbus.errors.MatchRuleInvalid; +import org.freedesktop.dbus.errors.ServiceUnknown; +import org.freedesktop.dbus.errors.UnknownMethod; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.DBus.NameOwnerChanged; +import org.freedesktop.dbus.interfaces.Debug; import org.freedesktop.dbus.interfaces.FatalException; import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.Monitoring; @@ -78,9 +81,24 @@ public class DBusDaemon extends Thread implements Closeable { private final AbstractTransport transport; + /** Whether the {@code org.freedesktop.DBus.Debug.Stats} interface is offered to clients. */ + private final boolean debugFeaturesEnabled; + public DBusDaemon(AbstractTransport _transport) { + this(_transport, false); + } + + /** + * Creates a new daemon. + * + * @param _transport transport to listen on + * @param _debugFeaturesEnabled whether to offer the {@code org.freedesktop.DBus.Debug.Stats} interface; a default + * daemon behaves like a production reference daemon and does not expose it + */ + public DBusDaemon(AbstractTransport _transport, boolean _debugFeaturesEnabled) { setName(getClass().getSimpleName() + "-Thread"); transport = _transport; + debugFeaturesEnabled = _debugFeaturesEnabled; names.put(DBUS_BUSNAME, null); } @@ -489,7 +507,7 @@ void updateThreadName() { } } - public class DBusServer implements DBus, Introspectable, Peer, Monitoring { + public class DBusServer implements DBus, Introspectable, Peer, Monitoring, Debug.Stats { private final String machineId; private ConnectionStruct connStruct; @@ -798,6 +816,21 @@ public String getObjectPath() { @Override public String Introspect() { + String debugStatsInterface = !debugFeaturesEnabled ? "" : """ + + + + + + + + + + + + + """; + return """ @@ -875,7 +908,7 @@ public String Introspect() { - """; + """ + debugStatsInterface + ""; } @Override @@ -913,6 +946,83 @@ public String GetMachineId() { return machineId; } + /** + * Guard for the {@code org.freedesktop.DBus.Debug.Stats} methods. On a daemon without debug features enabled + * the interface must appear as if it did not exist, mirroring a production reference daemon. + */ + private void requireDebugEnabled() { + if (!debugFeaturesEnabled) { + throw new UnknownMethod("This service does not implement org.freedesktop.DBus.Debug.Stats"); + } + } + + @Override + public Map> GetStats() { + requireDebugEnabled(); + + int totalRules = 0; + for (ConnectionStruct cs : conns.keySet()) { + synchronized (cs.rules) { + totalRules += cs.rules.size(); + } + } + + Map> stats = new LinkedHashMap<>(); + stats.put("ActiveConnections", new Variant<>(new UInt32(conns.size()))); + stats.put("BusNames", new Variant<>(new UInt32(names.size()))); + stats.put("MatchRules", new Variant<>(new UInt32(totalRules))); + stats.put("SerialNumber", new Variant<>(new UInt32(nextUnique.get()))); + return stats; + } + + @Override + public Map> GetConnectionStats(String _busName) { + requireDebugEnabled(); + + ConnectionStruct cs = names.get(_busName); + if (cs == null) { + throw new ServiceUnknown(String.format("The name `%s' does not exist", _busName)); + } + + int busNames = 0; + synchronized (names) { + for (ConnectionStruct owner : names.values()) { + if (owner == cs) { + busNames++; + } + } + } + + int matchRules; + synchronized (cs.rules) { + matchRules = cs.rules.size(); + } + + Map> stats = new LinkedHashMap<>(); + stats.put("UniqueName", new Variant<>(cs.unique)); + stats.put("MatchRules", new Variant<>(new UInt32(matchRules))); + stats.put("BusNames", new Variant<>(new UInt32(busNames))); + return stats; + } + + @Override + public Map GetAllMatchRules() { + requireDebugEnabled(); + + Map result = new LinkedHashMap<>(); + for (ConnectionStruct cs : conns.keySet()) { + if (cs.unique == null) { + continue; + } + String[] rules; + synchronized (cs.rules) { + rules = cs.rules.stream().map(DBusMatchRule::toString).toArray(String[]::new); + } + result.put(cs.unique, rules); + } + return result; + } + } public class DBusDaemonSenderThread extends Thread { diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DebuggableEmbeddedDBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DebuggableEmbeddedDBusDaemon.java new file mode 100644 index 000000000..4f8414de1 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DebuggableEmbeddedDBusDaemon.java @@ -0,0 +1,33 @@ +package org.freedesktop.dbus.bin; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.exceptions.InvalidBusAddressException; + +/** + * Variant of {@link EmbeddedDBusDaemon} which additionally offers the {@code org.freedesktop.DBus.Debug.Stats} + * interface (see {@link org.freedesktop.dbus.interfaces.Debug.Stats}). + *

+ * The reference {@code dbus-daemon} only exposes these debug/statistics interfaces when it was compiled with the + * corresponding support enabled. To keep the default {@link EmbeddedDBusDaemon} behaving like a production daemon, the + * debug features are only available through this dedicated subclass. Callers must knowingly decide which daemon variant + * they want to run/develop against. + *

+ *

+ * This daemon is intended for debugging and diagnostics only and must not be used in production. + *

+ */ +public class DebuggableEmbeddedDBusDaemon extends EmbeddedDBusDaemon { + + public DebuggableEmbeddedDBusDaemon(BusAddress _address) { + super(_address); + } + + public DebuggableEmbeddedDBusDaemon(String _address) throws InvalidBusAddressException { + super(_address); + } + + @Override + protected boolean isDebugStatsEnabled() { + return true; + } +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/EmbeddedDBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/EmbeddedDBusDaemon.java index 96d796b06..c83785061 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/EmbeddedDBusDaemon.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/EmbeddedDBusDaemon.java @@ -273,8 +273,21 @@ public void setBindCallback(Consumer _callback) { bindCallback = _callback; } + /** + * Whether this daemon offers the {@code org.freedesktop.DBus.Debug.Stats} interface. + *

+ * The default embedded daemon behaves like a production reference daemon and does not expose any debug interface. + * Subclasses may override this to enable debug features. + *

+ * + * @return {@code false} for the default embedded daemon + */ + protected boolean isDebugStatsEnabled() { + return false; + } + private synchronized void setDaemonAndStart(AbstractTransport _transport) { - daemon = new DBusDaemon(_transport); + daemon = new DBusDaemon(_transport, isDebugStatsEnabled()); daemon.start(); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index b4ceb9071..8ca7cca99 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -18,9 +18,7 @@ import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.DBusMonitorHandler; import org.freedesktop.dbus.interfaces.DBusSigHandler; -import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.ObjectManager; -import org.freedesktop.dbus.interfaces.Peer; import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.messages.*; diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Debug.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Debug.java new file mode 100644 index 000000000..a1fd68cf5 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Debug.java @@ -0,0 +1,60 @@ +package org.freedesktop.dbus.interfaces; + +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.types.Variant; + +import java.util.Map; + +/** + * Container for the {@code org.freedesktop.DBus.Debug.*} interfaces. + *

+ * These interfaces are only offered by the reference {@code dbus-daemon} when it was compiled with statistics/debug + * support enabled. They are meant purely for debugging and diagnostics and must not be relied upon in production. + *

+ */ +public interface Debug { + + /** + * The {@code org.freedesktop.DBus.Debug.Stats} interface exposes statistics about the message bus and its + * connections. + *

+ * In dbus-java these methods are only available on a message bus which was explicitly started with debug features + * enabled (see the debug-enabled variant of the embedded daemon). On a default embedded daemon - just like on a + * production reference daemon - calling these methods results in an + * {@code org.freedesktop.DBus.Error.UnknownMethod} error. + *

+ */ + @DBusInterfaceName("org.freedesktop.DBus.Debug.Stats") + @SuppressWarnings({"checkstyle:methodname"}) + interface Stats extends DBusInterface { + + /** + * Returns a set of statistics about the message bus as a whole. + *

+ * The exact set of keys is implementation-specific and may change between versions. Callers should treat unknown + * keys gracefully and must not assume that a particular key is present. + *

+ * + * @return statistics keyed by name + */ + Map> GetStats(); + + /** + * Returns a set of statistics about a single connection, identified by any of its bus names (unique or + * well-known). + * + * @param _busName bus name identifying the connection + * + * @return statistics keyed by name + */ + Map> GetConnectionStats(String _busName); + + /** + * Returns all match rules currently registered on the bus, grouped by the unique name of the connection that + * added them. + * + * @return map of unique connection name to its match rule strings + */ + Map GetAllMatchRules(); + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java new file mode 100644 index 000000000..5c43c70cc --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java @@ -0,0 +1,84 @@ +package org.freedesktop.dbus.bin; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.errors.ServiceUnknown; +import org.freedesktop.dbus.errors.UnknownMethod; +import org.freedesktop.dbus.interfaces.DBus; +import org.freedesktop.dbus.interfaces.Debug; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.freedesktop.dbus.types.UInt32; +import org.freedesktop.dbus.types.Variant; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Map; + +/** + * Verifies the {@code org.freedesktop.DBus.Debug.Stats} interface which is only offered by the + * {@link DebuggableEmbeddedDBusDaemon}, not by the default {@link EmbeddedDBusDaemon}. + */ +class DebugStatsTest extends AbstractBaseTest { + + private static final String DBUS_BUSNAME = "org.freedesktop.DBus"; + private static final String DBUS_BUSPATH = "/org/freedesktop/DBus"; + + @Test + void testDebugStatsAvailableOnDebuggableDaemon() throws Exception { + String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); + String newAddress = TransportBuilder.createDynamicSession(protocolType, false); + BusAddress busAddress = BusAddress.of(newAddress); + BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); + + try (DebuggableEmbeddedDBusDaemon daemon = new DebuggableEmbeddedDBusDaemon(listenBusAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { + Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); + + // global statistics contain the documented keys + Map> global = stats.GetStats(); + assertTrue(global.containsKey("ActiveConnections"), "ActiveConnections missing"); + assertTrue(global.containsKey("BusNames"), "BusNames missing"); + assertTrue(global.containsKey("MatchRules"), "MatchRules missing"); + assertInstanceOf(UInt32.class, global.get("ActiveConnections").getValue()); + + // add a match rule and ensure it is reported by GetAllMatchRules + DBus dbus = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, DBus.class); + dbus.AddMatch("type='signal',interface='com.example.Foo'"); + + Map allRules = stats.GetAllMatchRules(); + boolean found = allRules.values().stream().flatMap(Arrays::stream) + .anyMatch(r -> r.contains("com.example.Foo")); + assertTrue(found, "added match rule should be listed by GetAllMatchRules"); + + // per-connection statistics for our own unique name + Map> connStats = stats.GetConnectionStats(conn.getUniqueName()); + assertEquals(conn.getUniqueName(), connStats.get("UniqueName").getValue()); + + // unknown connection name -> error + assertThrows(ServiceUnknown.class, () -> stats.GetConnectionStats("com.does.not.Exist")); + } + } + } + + @Test + void testDebugStatsUnavailableOnDefaultDaemon() throws Exception { + String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); + String newAddress = TransportBuilder.createDynamicSession(protocolType, false); + BusAddress busAddress = BusAddress.of(newAddress); + BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenBusAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { + Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); + // a default daemon behaves like a production reference daemon: the interface does not exist + assertThrows(UnknownMethod.class, stats::GetStats); + } + } + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java index 1226549ef..7d82bb819 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/ObjectManagerTest.java @@ -6,14 +6,12 @@ import org.freedesktop.dbus.annotations.DBusProperty.Access; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.ObjectManager; import org.freedesktop.dbus.types.Variant; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; From 0cfe3998771abb2c52da6633ab15214986c5ca1d Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 26 Jul 2026 15:16:10 +0200 Subject: [PATCH 29/38] More features for DBus Daemon Added reloadConfig method --- .../org/freedesktop/dbus/bin/DBusDaemon.java | 126 +++++++++++++++++- .../org/freedesktop/dbus/interfaces/DBus.java | 28 ++++ .../dbus/bin/BusInterfaceExtrasTest.java | 121 +++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java index 86083a00f..6efd20370 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java @@ -8,8 +8,10 @@ import org.freedesktop.dbus.connections.transports.TransportConnection; import org.freedesktop.dbus.errors.AccessDenied; import org.freedesktop.dbus.errors.MatchRuleInvalid; +import org.freedesktop.dbus.errors.PropertyReadOnly; import org.freedesktop.dbus.errors.ServiceUnknown; import org.freedesktop.dbus.errors.UnknownMethod; +import org.freedesktop.dbus.errors.UnknownProperty; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBus; @@ -19,6 +21,7 @@ import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.Monitoring; import org.freedesktop.dbus.interfaces.Peer; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.matchrules.MatchRuleParser; import org.freedesktop.dbus.messages.DBusSignal; @@ -507,7 +510,7 @@ void updateThreadName() { } } - public class DBusServer implements DBus, Introspectable, Peer, Monitoring, Debug.Stats { + public class DBusServer implements DBus, Introspectable, Peer, Monitoring, Properties, Debug.Stats { private final String machineId; private ConnectionStruct connStruct; @@ -745,6 +748,14 @@ private void handleMessage(ConnectionStruct _connStruct, Message _msg) throws DB return; } + // Properties.Get/GetAll/Set are handled explicitly: the reflective dispatch below cannot marshal the + // generic return type of Properties.Get (its wire signature is a VARIANT "v"). + if ("Get".equals(_msg.getName()) || "GetAll".equals(_msg.getName()) || "Set".equals(_msg.getName())) { + this.connStruct = _connStruct; + handlePropertiesCall(_connStruct, (MethodCall) _msg, args, messageFactory); + return; + } + try { meth = DBusServer.class.getMethod(_msg.getName(), cs); try { @@ -907,6 +918,31 @@ public String Introspect() { + + + + + + + + + + + + + + + + + + + + + + + + + """ + debugStatsInterface + ""; } @@ -946,6 +982,94 @@ public String GetMachineId() { return machineId; } + @Override + public void ReloadConfig() { + // the embedded daemon has no configuration file to reload + } + + @Override + @SuppressWarnings("unchecked") + public A Get(String _interfaceName, String _propertyName) { + return (A) getBusProperty(_interfaceName, _propertyName); + } + + @Override + public void Set(String _interfaceName, String _propertyName, A _value) { + throw new PropertyReadOnly("Property " + _propertyName + " is read only"); + } + + @Override + public Map> GetAll(String _interfaceName) { + Map> result = new LinkedHashMap<>(); + if (DBUS_BUSNAME.equals(_interfaceName)) { + result.put("Features", new Variant<>(busFeatures())); + result.put("Interfaces", new Variant<>(busInterfaces())); + } + return result; + } + + /** + * Handles a {@code org.freedesktop.DBus.Properties} call against the bus object, marshalling the reply with the + * correct wire signatures ("v" for Get, "a{sv}" for GetAll). + */ + private void handlePropertiesCall(ConnectionStruct _connStruct, MethodCall _msg, Object[] _args, MessageFactory _messageFactory) throws DBusException { + try { + switch (_msg.getName()) { + case "Get" -> { + Object value = getBusProperty((String) _args[0], (String) _args[1]); + send(_connStruct, _messageFactory.createMethodReturn(DBUS_BUSNAME, _msg, "v", new Variant<>(value)), true); + } + case "GetAll" -> { + Map> all = GetAll((String) _args[0]); + send(_connStruct, _messageFactory.createMethodReturn(DBUS_BUSNAME, _msg, "a{sv}", all), true); + } + case "Set" -> Set((String) _args[0], (String) _args[1], _args.length > 2 ? _args[2] : null); + default -> throw new UnknownMethod("This service does not support " + _msg.getName()); + } + } catch (DBusExecutionException _ex) { + LOGGER.debug("", _ex); + send(_connStruct, _messageFactory.createError(DBUS_BUSNAME, _msg, _ex)); + } + } + + /** + * Returns the value of a property of the bus object. Only the {@code org.freedesktop.DBus} interface exposes + * properties ({@code Features} and {@code Interfaces}). + */ + private Object getBusProperty(String _interfaceName, String _propertyName) { + if (DBUS_BUSNAME.equals(_interfaceName)) { + switch (_propertyName) { + case "Features": + return busFeatures(); + case "Interfaces": + return busInterfaces(); + default: + } + } + throw new UnknownProperty(String.format("No such property '%s' on interface '%s'", _propertyName, _interfaceName)); + } + + /** + * The bus features. The embedded daemon supports none of the reference features (header filtering, systemd + * activation, SELinux/AppArmor mediation), so an empty array is returned (which the specification permits). + */ + private String[] busFeatures() { + return EMPTY_STRING_ARRAY; + } + + /** + * The optional interfaces implemented by the bus object in addition to the core {@code org.freedesktop.DBus} + * interface (excluding Peer/Properties/Introspectable per specification). + */ + private String[] busInterfaces() { + List ifaces = new ArrayList<>(); + ifaces.add("org.freedesktop.DBus.Monitoring"); + if (debugFeaturesEnabled) { + ifaces.add("org.freedesktop.DBus.Debug.Stats"); + } + return ifaces.toArray(EMPTY_STRING_ARRAY); + } + /** * Guard for the {@code org.freedesktop.DBus.Debug.Stats} methods. On a daemon without debug features enabled * the interface must appear as if it did not exist, mirroring a production reference daemon. diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBus.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBus.java index de04762ab..3175a2b78 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBus.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/DBus.java @@ -202,6 +202,14 @@ public interface DBus extends DBusInterface { */ String GetId(); + /** + * Reloads the bus daemon's configuration. + *

+ * For daemons that do not use a configuration file (such as the embedded daemon) this is a no-op. + *

+ */ + void ReloadConfig(); + /** * Signal sent when the owner of a name changes */ @@ -263,4 +271,24 @@ public String toString() { } + /** + * Signal sent when the list of activatable services on the bus changes. + *

+ * The embedded daemon does not perform service activation and therefore never emits this signal; it is declared + * for spec completeness so clients may subscribe to it. + *

+ */ + class ActivatableServicesChanged extends DBusSignal { + + public ActivatableServicesChanged(String _path) throws DBusException { + super(_path); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " []"; + } + + } + } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java new file mode 100644 index 000000000..da7878add --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java @@ -0,0 +1,121 @@ +package org.freedesktop.dbus.bin; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.errors.PropertyReadOnly; +import org.freedesktop.dbus.errors.UnknownProperty; +import org.freedesktop.dbus.interfaces.DBus; +import org.freedesktop.dbus.interfaces.Introspectable; +import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.freedesktop.dbus.types.Variant; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Verifies the additional standard {@code org.freedesktop.DBus} bus features: + *
    + *
  • D3 - the {@code ActivatableServicesChanged} signal is declared/introspected
  • + *
  • D4 - the {@code ReloadConfig} method exists (no-op on the embedded daemon)
  • + *
  • D5 - the bus properties {@code Features} and {@code Interfaces}
  • + *
+ */ +class BusInterfaceExtrasTest extends AbstractBaseTest { + + private static final String DBUS_BUSNAME = "org.freedesktop.DBus"; + private static final String DBUS_BUSPATH = "/org/freedesktop/DBus"; + + @Test + void testBusInterfaceExtrasOnDefaultDaemon() throws Exception { + withDaemon(false, conn -> { + // D3 + D5: the introspection data declares the new signal, the Properties interface and the properties + Introspectable intro = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Introspectable.class); + String xml = intro.Introspect(); + assertTrue(xml.contains("ActivatableServicesChanged"), "ActivatableServicesChanged signal missing"); + assertTrue(xml.contains("org.freedesktop.DBus.Properties"), "Properties interface missing"); + assertTrue(xml.contains("name=\"Features\""), "Features property missing"); + assertTrue(xml.contains("name=\"Interfaces\""), "Interfaces property missing"); + + // D4: ReloadConfig is a no-op and must not fail + DBus dbus = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, DBus.class); + assertDoesNotThrow(dbus::ReloadConfig); + + // D5: Features is empty, Interfaces contains Monitoring but not Debug.Stats (debug disabled) + Properties props = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Properties.class); + assertEquals(List.of(), asStringList(props.Get(DBUS_BUSNAME, "Features"))); + + List interfaces = asStringList(props.Get(DBUS_BUSNAME, "Interfaces")); + assertTrue(interfaces.contains("org.freedesktop.DBus.Monitoring"), "Monitoring should be listed"); + assertFalse(interfaces.contains("org.freedesktop.DBus.Debug.Stats"), "Debug.Stats must not be listed on default daemon"); + + // GetAll returns both properties + Map> all = props.GetAll(DBUS_BUSNAME); + assertTrue(all.containsKey("Features"), "GetAll missing Features"); + assertTrue(all.containsKey("Interfaces"), "GetAll missing Interfaces"); + + // unknown property -> error, properties are read only + assertThrows(UnknownProperty.class, () -> props.Get(DBUS_BUSNAME, "DoesNotExist")); + assertThrows(PropertyReadOnly.class, () -> props.Set(DBUS_BUSNAME, "Features", new String[0])); + }); + } + + @Test + void testInterfacesPropertyListsDebugStatsOnDebuggableDaemon() throws Exception { + withDaemon(true, conn -> { + Properties props = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Properties.class); + List interfaces = asStringList(props.Get(DBUS_BUSNAME, "Interfaces")); + assertTrue(interfaces.contains("org.freedesktop.DBus.Debug.Stats"), + "Debug.Stats should be listed on a debuggable daemon, got: " + interfaces); + }); + } + + /** + * Normalizes a value that may be a {@link Variant} wrapping an array/list of strings (or the array/list directly) + * into a {@link List} of strings. + */ + private static List asStringList(Object _value) { + Object val = _value instanceof Variant v ? v.getValue() : _value; + List result = new ArrayList<>(); + if (val instanceof Object[] arr) { + for (Object o : arr) { + result.add(String.valueOf(o)); + } + } else if (val instanceof Collection col) { + for (Object o : col) { + result.add(String.valueOf(o)); + } + } + return result; + } + + private void withDaemon(boolean _debug, ConnectionConsumer _consumer) throws IOException { + String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); + String newAddress = TransportBuilder.createDynamicSession(protocolType, false); + BusAddress busAddress = BusAddress.of(newAddress); + BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = _debug + ? new DebuggableEmbeddedDBusDaemon(listenBusAddress) + : new EmbeddedDBusDaemon(listenBusAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { + _consumer.accept(conn); + } catch (Exception _ex) { + fail(_ex); + } + } + } + + @FunctionalInterface + private interface ConnectionConsumer { + void accept(DBusConnection _conn) throws Exception; + } +} From 4f04f5a4366dd4d980dc664964d6ba2362efa654 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 26 Jul 2026 15:55:17 +0200 Subject: [PATCH 30/38] Added support for tcp-nonce --- .../transports/TransportBuilder.java | 29 +++-- .../spi/transport/ITransportProvider.java | 18 +++ .../transport/tcp/NonceTcpTransportTest.java | 90 +++++++++++++++ .../dbus/transport/tcp/TcpBusAddress.java | 22 ++++ .../dbus/transport/tcp/TcpTransport.java | 106 ++++++++++++++++++ .../transport/tcp/TcpTransportProvider.java | 6 + src/site/markdown/index.md | 8 ++ 7 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java index d40a3e56b..71886d9b8 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java @@ -52,23 +52,30 @@ static void findTransportProvider(ClassLoader _clzLoader, ModuleLayer _layer) { ? ServiceLoader.load(_layer, ITransportProvider.class) : ServiceLoader.load(ITransportProvider.class, _clzLoader); for (ITransportProvider provider : spiLoader) { - String providerBusType = provider.getSupportedBusType(); - if (providerBusType == null) { // invalid transport, ignore + List providerBusTypes = provider.getSupportedBusTypes(); + if (providerBusTypes == null || providerBusTypes.isEmpty()) { // invalid transport, ignore LOGGER.warn("Transport {} is invalid: No bustype configured", provider.getClass()); continue; } - providerBusType = providerBusType.toUpperCase(Locale.US); - LOGGER.debug("Found provider '{}' named '{}' providing bustype '{}'", provider.getClass().getSimpleName(), provider.getTransportName(), providerBusType); + for (String rawBusType : providerBusTypes) { + if (rawBusType == null) { // skip invalid entries + LOGGER.warn("Transport {} provides a null bustype, ignoring that entry", provider.getClass()); + continue; + } + String providerBusType = rawBusType.toUpperCase(Locale.US); + + LOGGER.debug("Found provider '{}' named '{}' providing bustype '{}'", provider.getClass().getSimpleName(), provider.getTransportName(), providerBusType); - if (PROVIDERS.containsKey(key) && PROVIDERS.get(key).containsKey(providerBusType)) { - throw new TransportRegistrationException("Found transport " - + PROVIDERS.get(key).get(providerBusType).getClass().getName() - + " and " - + provider.getClass().getName() + " both providing transport for socket type " - + providerBusType + ", please only add one of them to classpath."); + if (PROVIDERS.containsKey(key) && PROVIDERS.get(key).containsKey(providerBusType)) { + throw new TransportRegistrationException("Found transport " + + PROVIDERS.get(key).get(providerBusType).getClass().getName() + + " and " + + provider.getClass().getName() + " both providing transport for socket type " + + providerBusType + ", please only add one of them to classpath."); + } + PROVIDERS.computeIfAbsent(key, x -> new HashMap<>()).put(providerBusType, provider); } - PROVIDERS.computeIfAbsent(key, x -> new HashMap<>()).put(providerBusType, provider); } if (PROVIDERS.isEmpty()) { throw new TransportRegistrationException("No dbus-java-transport found in classpath, please add a transport module"); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/transport/ITransportProvider.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/transport/ITransportProvider.java index d9d38676e..f53f91066 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/transport/ITransportProvider.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/spi/transport/ITransportProvider.java @@ -5,6 +5,8 @@ import org.freedesktop.dbus.connections.transports.AbstractTransport; import org.freedesktop.dbus.exceptions.TransportConfigurationException; +import java.util.List; + import javax.xml.transform.TransformerConfigurationException; /** @@ -44,6 +46,22 @@ public interface ITransportProvider { */ String getSupportedBusType(); + /** + * All bus types supported by this provider. + *

+ * By default this returns the single type reported by {@link #getSupportedBusType()}. A provider that serves + * multiple related address schemes (for example {@code tcp} and {@code nonce-tcp}) can override this to register + * for all of them without requiring a separate provider per scheme. + *

+ * + * @return list of supported bus types, never null or empty + * + * @since 6.0.0 + */ + default List getSupportedBusTypes() { + return List.of(getSupportedBusType()); + } + /** * Creates a new (dynamic) session for this transport. * diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java new file mode 100644 index 000000000..77320f965 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java @@ -0,0 +1,90 @@ +package org.freedesktop.dbus.transport.tcp; + +import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Verifies the {@code nonce-tcp} transport: a client must present the 16-byte nonce read from the + * {@code noncefile} before the D-Bus/SASL handshake, otherwise the connection is rejected. + *

+ * These tests require the TCP transport on the classpath (only present in the TCP test run) and are disabled + * otherwise. + *

+ */ +@EnabledIf("isNonceTcpAvailable") +class NonceTcpTransportTest extends AbstractBaseTest { + + @Test + void testNonceTcpConnectSucceedsWithValidNonce() throws Exception { + Path nonceFile = Files.createTempFile("dbus-nonce-", ".tmp"); + try { + String base = nonceTcpBase(nonceFile); + BusAddress connectAddress = BusAddress.of(base); + BusAddress listenAddress = BusAddress.of(base + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "connection should be established with a valid nonce"); + } + } + } finally { + Files.deleteIfExists(nonceFile); + } + } + + @Test + void testNonceTcpConnectFailsWithWrongNonce() throws Exception { + Path nonceFile = Files.createTempFile("dbus-nonce-", ".tmp"); + try { + String base = nonceTcpBase(nonceFile); + BusAddress connectAddress = BusAddress.of(base); + BusAddress listenAddress = BusAddress.of(base + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + // corrupt the nonce file so the client sends a nonce that does not match the server's + Files.write(nonceFile, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}); + + assertThrows(Exception.class, () -> { + // use a short timeout to keep connection retries (and thus the test) brief + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress) + .transportConfig().withTimeout(1000).back() + .withShared(false).build()) { + fail("connection must be rejected when the nonce is invalid"); + } + }); + } + } finally { + Files.deleteIfExists(nonceFile); + } + } + + /** + * Condition for {@link EnabledIf}: the nonce-tcp transport is only available when the TCP transport is on the + * classpath (which is only the case in the TCP test run). + */ + static boolean isNonceTcpAvailable() { + return TransportBuilder.getRegisteredBusTypes().contains("NONCE-TCP"); + } + + /** + * Builds a {@code nonce-tcp} base address (host/port/guid taken from a dynamic TCP session) pointing at the + * given nonce file. + */ + private static String nonceTcpBase(Path _nonceFile) { + String tcp = TransportBuilder.createDynamicSession("TCP", false); + return tcp.replaceFirst("^tcp:", "nonce-tcp:") + ",noncefile=" + _nonceFile; + } +} diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java index d05038190..ecd952c39 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java @@ -27,4 +27,26 @@ public int getPort() { return Util.isValidNetworkPort(getParameterValue("port"), true) ? Integer.parseInt(getParameterValue("port")) : DEFAULT_PORT; } + /** + * Whether this is a {@code nonce-tcp} address (i.e. requires the nonce authentication handshake). + * + * @return true for nonce-tcp addresses + */ + public boolean isNonceTcp() { + return isBusType("nonce-tcp"); + } + + public boolean hasNonceFile() { + return hasParameter("noncefile"); + } + + /** + * The path to the nonce file as given by the {@code noncefile} address parameter (may be {@code null}). + * + * @return nonce file path or null + */ + public String getNonceFile() { + return getParameterValue("noncefile"); + } + } diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java index 7ec9a1058..5731a6195 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java @@ -4,11 +4,19 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractTransport; +import org.freedesktop.dbus.exceptions.AuthenticationException; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Set; /** * Transport type representing a transport connection to TCP. @@ -18,11 +26,17 @@ */ public class TcpTransport extends AbstractTransport { + /** Length of the nonce exchanged for the {@code nonce-tcp} transport (D-Bus specification). */ + private static final int NONCE_LENGTH = 16; + private final int timeout; private SocketChannel socket; private ServerSocketChannel serverSocket; + /** The nonce generated by the server (nonce-tcp only). */ + private byte[] serverNonce; + TcpTransport(BusAddress _address, int _timeout, TransportConfig _config) { super(_address, _config); timeout = _timeout; @@ -63,12 +77,19 @@ protected void bindImpl() throws IOException { getAddress().getPort()); serverSocket.bind(socketAddress); + + if (getAddress().isNonceTcp()) { + writeNonceFile(); + } } } @Override protected SocketChannel acceptImpl() throws IOException { socket = serverSocket.accept(); + if (getAddress().isNonceTcp()) { + verifyClientNonce(socket); + } return socket; } @@ -93,9 +114,94 @@ public SocketChannel connectImpl() throws IOException { getLogger().debug("Connected to {} using local port {} and remote port {}", getAddress().getHost(), getAddress().getPort(), socket.socket().getLocalPort()); + if (getAddress().isNonceTcp()) { + // the nonce read from the noncefile must be sent before anything else (i.e. before SASL) + sendClientNonce(socket); + } + return socket; } + /** + * Generates a random nonce and writes it to the configured nonce file (server side). + * + * @throws IOException if no nonce file is configured or writing fails + */ + private void writeNonceFile() throws IOException { + String nonceFile = getAddress().getNonceFile(); + if (nonceFile == null) { + throw new IOException("nonce-tcp listening address requires a 'noncefile' parameter"); + } + + serverNonce = new byte[NONCE_LENGTH]; + new SecureRandom().nextBytes(serverNonce); + + Path path = Path.of(nonceFile); + Files.write(path, serverNonce); + // best effort: restrict the nonce file to the owner (ignored on non-POSIX file systems) + try { + Files.setPosixFilePermissions(path, Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + } catch (UnsupportedOperationException | IOException _ex) { + getLogger().debug("Could not restrict permissions of nonce file {}", nonceFile, _ex); + } + getLogger().debug("Wrote nonce file {}", nonceFile); + } + + /** + * Reads the client nonce from the accepted connection and compares it to the server nonce. + * + * @param _channel accepted client channel + * @throws IOException if the nonce does not match or cannot be read + */ + private void verifyClientNonce(SocketChannel _channel) throws IOException { + byte[] received = readFully(_channel, NONCE_LENGTH); + if (serverNonce == null || !Arrays.equals(serverNonce, received)) { + _channel.close(); + // AuthenticationException is handled by the accept loop: the offending connection is dropped + // while the daemon keeps listening for other clients + throw new AuthenticationException("Rejected connection: invalid nonce received"); + } + getLogger().trace("Client provided a valid nonce"); + } + + /** + * Reads the nonce from the configured nonce file and sends it on the given channel (client side). + * + * @param _channel connected channel + * @throws IOException if the nonce file is missing/too short or writing fails + */ + private void sendClientNonce(SocketChannel _channel) throws IOException { + String nonceFile = getAddress().getNonceFile(); + if (nonceFile == null) { + throw new IOException("nonce-tcp address requires a 'noncefile' parameter"); + } + + byte[] fileContent = Files.readAllBytes(Path.of(nonceFile)); + if (fileContent.length < NONCE_LENGTH) { + throw new IOException("Nonce file " + nonceFile + " is too short (expected at least " + NONCE_LENGTH + " bytes)"); + } + + writeFully(_channel, Arrays.copyOf(fileContent, NONCE_LENGTH)); + getLogger().trace("Sent nonce from {}", nonceFile); + } + + private static byte[] readFully(SocketChannel _channel, int _length) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(_length); + while (buf.hasRemaining()) { + if (_channel.read(buf) < 0) { + throw new IOException("Connection closed while reading nonce (got " + buf.position() + " of " + _length + " bytes)"); + } + } + return buf.array(); + } + + private static void writeFully(SocketChannel _channel, byte[] _data) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(_data); + while (buf.hasRemaining()) { + _channel.write(buf); + } + } + @Override protected void closeTransport() throws IOException { if (socket != null && socket.isOpen()) { diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java index 279b73d73..a612616f3 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransportProvider.java @@ -10,6 +10,7 @@ import java.net.ServerSocket; import java.security.SecureRandom; +import java.util.List; import java.util.Random; public class TcpTransportProvider implements ITransportProvider { @@ -41,6 +42,11 @@ public String getSupportedBusType() { return "TCP"; } + @Override + public List getSupportedBusTypes() { + return List.of(getSupportedBusType(), "NONCE-TCP"); + } + @Override public String createDynamicSessionAddress(boolean _listeningSocket) { String address = "tcp:host=localhost"; diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 4a318465e..a9cbe9ba9 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -36,6 +36,14 @@ A typical dependency set is `dbus-java-core` + one transport. See the [README](https://github.com/hypfvieh/dbus-java#how-to-use-file-descriptors) for guidance on choosing a transport (for example when you need file descriptor support). +### Transports that are not provided + +The `unixexec:` transport is intentionally not provided. Its model (spawning a helper and +speaking D-Bus over that process' stdin/stdout) does not fit dbus-java's `SocketChannel`-based +transport architecture, and the practical use cases are too rare to justify the effort. For the +most common scenario - tunnelling D-Bus over SSH - a separate, third-party transport based on +SSHj already exists. + ## Where to go next * [Quickstart](./quick-start.html) - add the dependencies and open a connection From e9006d10abf64e9e00bf7e29524a9b57be243475 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 26 Jul 2026 16:43:10 +0200 Subject: [PATCH 31/38] Added support for additional bus address parameters --- .../transports/UnixServerAddressResolver.java | 80 +++++++++++++ .../transports/UnixDirListenTest.java | 69 +++++++++++ .../UnixServerAddressResolverTest.java | 84 ++++++++++++++ .../dbus/transport/tcp/TcpFamilyBindTest.java | 107 ++++++++++++++++++ .../transport/jnr/UnixSocketTransport.java | 4 + .../junixsocket/JUnixSocketUnixTransport.java | 4 + .../jre/NativeUnixSocketTransport.java | 4 + .../dbus/transport/tcp/TcpBusAddress.java | 27 +++++ .../dbus/transport/tcp/TcpTransport.java | 80 ++++++++++++- 9 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixDirListenTest.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java new file mode 100644 index 000000000..259635a9c --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java @@ -0,0 +1,80 @@ +package org.freedesktop.dbus.connections.transports; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; + +import java.io.File; +import java.security.SecureRandom; + +/** + * Resolves the listen-side unix address parameters {@code dir}, {@code tmpdir} and {@code runtime} into a concrete + * {@code path} (or {@code abstract}) as described by the D-Bus specification. + *

+ * These parameters may only be used in server (listening) addresses; the resulting client address will contain the + * concrete {@code path}/{@code abstract} instead. For client addresses (or addresses that already provide a concrete + * {@code path}/{@code abstract}) this resolver does nothing. + *

+ * + * @since 6.0.0 + */ +public final class UnixServerAddressResolver { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int RANDOM_CHARS = 10; + + private UnixServerAddressResolver() { + } + + /** + * Resolves {@code dir}/{@code tmpdir}/{@code runtime} on the given listening unix address into a concrete + * {@code path} or {@code abstract} parameter (added to the address in place). + * + * @param _address unix bus address + * @param _supportsAbstract whether the transport supports abstract sockets (used for {@code tmpdir}) + * + * @throws TransportConfigurationException if a parameter value is invalid or the environment is incomplete + */ + public static void resolve(BusAddress _address, boolean _supportsAbstract) throws TransportConfigurationException { + // only listening addresses carry dir/tmpdir/runtime, and only when no concrete socket was given + if (!_address.isListeningSocket() || _address.hasParameter("path") || _address.hasParameter("abstract")) { + return; + } + + if (_address.hasParameter("runtime")) { + String runtime = _address.getParameterValue("runtime"); + if (!"yes".equals(runtime)) { + throw new TransportConfigurationException("unix address parameter 'runtime' only accepts the value 'yes'"); + } + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + if (xdgRuntimeDir == null || xdgRuntimeDir.isBlank()) { + throw new TransportConfigurationException("runtime=yes requires the XDG_RUNTIME_DIR environment variable to be set"); + } + _address.addParameter("path", new File(xdgRuntimeDir, "bus").getAbsolutePath()); + } else if (_address.hasParameter("dir")) { + _address.addParameter("path", randomSocketPath(_address.getParameterValue("dir"))); + } else if (_address.hasParameter("tmpdir")) { + String tmpDir = _address.getParameterValue("tmpdir"); + if (_supportsAbstract) { + _address.addParameter("abstract", randomSocketPath(tmpDir)); + } else { + _address.addParameter("path", randomSocketPath(tmpDir)); + } + } + // no dir/tmpdir/runtime present: leave as-is, the transport will report the missing path/abstract + } + + /** + * Creates a not-yet-existing socket path with a random {@code dbus-XXXXXXXXXX} file name in the given directory. + */ + private static String randomSocketPath(String _dir) { + File file; + do { + StringBuilder sb = new StringBuilder("dbus-"); + for (int i = 0; i < RANDOM_CHARS; i++) { + sb.append((char) (RANDOM.nextInt(26) + 'A')); + } + file = new File(_dir, sb.toString()); + } while (file.exists()); + return file.getAbsolutePath(); + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixDirListenTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixDirListenTest.java new file mode 100644 index 000000000..9c62cab1f --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixDirListenTest.java @@ -0,0 +1,69 @@ +package org.freedesktop.dbus.connections.transports; + +import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * End-to-end test that a unix server can listen using the {@code dir} parameter (A5): the transport must resolve + * {@code dir} into a concrete socket path, create the socket there, and accept a client connecting to that path. + *

+ * Requires one of the unix transports on the classpath; disabled in the TCP run. + *

+ */ +@EnabledIf("isUnixAvailable") +class UnixDirListenTest extends AbstractBaseTest { + + static boolean isUnixAvailable() { + return TransportBuilder.getRegisteredBusTypes().contains("UNIX"); + } + + @Test + void testListenUsingDirParameter() throws Exception { + Path dir = Files.createTempDirectory("dbus-dir-test-"); + try { + BusAddress listenAddress = BusAddress.of("unix:dir=" + dir + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + // the transport must have created a "dbus-..." socket inside the directory + Path socket = findSocket(dir); + assertNotNull(socket, "server should have created a socket inside the dir"); + + BusAddress connectAddress = BusAddress.of("unix:path=" + socket); + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "client should connect to the dir-based socket"); + } + } + } finally { + deleteRecursively(dir); + } + } + + private static Path findSocket(Path _dir) throws IOException { + try (Stream files = Files.list(_dir)) { + return files.filter(p -> p.getFileName().toString().startsWith("dbus-")).findFirst().orElse(null); + } + } + + private static void deleteRecursively(Path _dir) throws IOException { + try (Stream walk = Files.walk(_dir)) { + List paths = walk.sorted(Comparator.reverseOrder()).toList(); + for (Path p : paths) { + Files.deleteIfExists(p); + } + } + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java new file mode 100644 index 000000000..cf0cb25e3 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java @@ -0,0 +1,84 @@ +package org.freedesktop.dbus.connections.transports; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; + +import java.io.File; + +/** + * Unit tests for {@link UnixServerAddressResolver} (A5): resolution of the listen-side unix address parameters + * {@code dir}, {@code tmpdir} and {@code runtime} into a concrete {@code path}/{@code abstract}. + */ +class UnixServerAddressResolverTest extends AbstractBaseTest { + + private static final String TMP = System.getProperty("java.io.tmpdir"); + + @Test + void testDirResolvesToRandomPath() throws Exception { + BusAddress address = BusAddress.of("unix:dir=" + TMP + ",listen=true"); + UnixServerAddressResolver.resolve(address, false); + + assertTrue(address.hasParameter("path"), "path should have been resolved"); + String path = address.getParameterValue("path"); + assertTrue(path.startsWith(new File(TMP, "dbus-").getAbsolutePath()), "unexpected path: " + path); + assertFalse(address.hasParameter("abstract"), "no abstract expected for dir"); + } + + @Test + void testTmpdirUsesAbstractWhenSupported() throws Exception { + BusAddress address = BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"); + UnixServerAddressResolver.resolve(address, true); + + assertTrue(address.hasParameter("abstract"), "abstract should have been resolved for tmpdir when supported"); + assertFalse(address.hasParameter("path"), "no path expected when abstract is used"); + } + + @Test + void testTmpdirUsesPathWhenAbstractUnsupported() throws Exception { + BusAddress address = BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"); + UnixServerAddressResolver.resolve(address, false); + + assertTrue(address.hasParameter("path"), "path should have been resolved for tmpdir when abstract unsupported"); + assertFalse(address.hasParameter("abstract"), "no abstract expected when unsupported"); + } + + @Test + void testRuntimeYes() throws Exception { + BusAddress address = BusAddress.of("unix:runtime=yes,listen=true"); + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + + if (xdgRuntimeDir != null && !xdgRuntimeDir.isBlank()) { + UnixServerAddressResolver.resolve(address, false); + assertEquals(new File(xdgRuntimeDir, "bus").getAbsolutePath(), address.getParameterValue("path")); + } else { + // without XDG_RUNTIME_DIR the resolver must reject runtime=yes + assertThrows(TransportConfigurationException.class, () -> UnixServerAddressResolver.resolve(address, false)); + } + } + + @Test + void testRuntimeRejectsInvalidValue() { + BusAddress address = BusAddress.of("unix:runtime=nope,listen=true"); + assertThrows(TransportConfigurationException.class, () -> UnixServerAddressResolver.resolve(address, false)); + } + + @Test + void testClientAddressIsNotResolved() throws Exception { + // no listen=true -> client address; dir must be left untouched + BusAddress address = BusAddress.of("unix:dir=" + TMP); + UnixServerAddressResolver.resolve(address, false); + + assertFalse(address.hasParameter("path"), "client address must not be resolved"); + assertFalse(address.hasParameter("abstract"), "client address must not be resolved"); + } + + @Test + void testExistingPathIsKept() throws Exception { + BusAddress address = BusAddress.of("unix:path=/tmp/existing.sock,listen=true,dir=" + TMP); + UnixServerAddressResolver.resolve(address, false); + + assertEquals("/tmp/existing.sock", address.getParameterValue("path"), "existing path must be kept"); + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java new file mode 100644 index 000000000..f4e1c6439 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java @@ -0,0 +1,107 @@ +package org.freedesktop.dbus.transport.tcp; + +import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.freedesktop.dbus.utils.Util; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.io.IOException; +import java.net.ServerSocket; + +/** + * Verifies the TCP transport address parameters {@code bind} and {@code family}. + *

+ * These tests require the TCP transport on the classpath (only present in the TCP test run) and are disabled + * otherwise. + *

+ */ +@EnabledIf("isTcpAvailable") +class TcpFamilyBindTest extends AbstractBaseTest { + + static boolean isTcpAvailable() { + return TransportBuilder.getRegisteredBusTypes().contains("TCP"); + } + + @Test + void testBindAddressIsUsedInsteadOfHost() throws Exception { + int port = freePort(); + String guid = Util.genGUID(); + + // advertised host is 127.0.0.1, but the server must actually bind to 127.0.0.2 (both are loopback on Linux). + // Without honouring 'bind' the server would listen on 127.0.0.1 and the connect below would be refused. + BusAddress listenAddress = BusAddress.of("tcp:host=127.0.0.1,port=" + port + ",guid=" + guid + ",bind=127.0.0.2,listen=true"); + BusAddress connectAddress = BusAddress.of("tcp:host=127.0.0.2,port=" + port + ",guid=" + guid); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "server must bind to the 'bind' address, not the advertised host"); + } + } + } + + @Test + void testBindToAllInterfaces() throws Exception { + String base = TransportBuilder.createDynamicSession("TCP", false); + BusAddress listenAddress = BusAddress.of(base + ",bind=*,listen=true"); + BusAddress connectAddress = BusAddress.of(base); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "connection should succeed against a wildcard-bound server"); + } + } + } + + @Test + void testExplicitIpv4Family() throws Exception { + String base = TransportBuilder.createDynamicSession("TCP", false); + BusAddress listenAddress = BusAddress.of(base + ",family=ipv4,listen=true"); + BusAddress connectAddress = BusAddress.of(base + ",family=ipv4"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "connection should succeed when forcing IPv4"); + } + } + } + + @Test + void testUnsupportedFamilyFails() throws Exception { + // start a working server, then attempt to connect to the very same address but with an invalid family. + // A running server ensures the connection would succeed if the family were ignored, so the expected failure + // can only come from the family validation itself. + String base = TransportBuilder.createDynamicSession("TCP", false); + BusAddress listenAddress = BusAddress.of(base + ",listen=true"); + BusAddress connectAddress = BusAddress.of(base + ",family=bogus"); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + assertThrows(Exception.class, () -> { + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress) + .transportConfig().withTimeout(1000).back() + .withShared(false).build()) { + fail("connection must fail for an unsupported address family"); + } + }); + } + } + + private static int freePort() throws IOException { + try (ServerSocket s = new ServerSocket()) { + s.bind(null); + return s.getLocalPort(); + } + } +} diff --git a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java index 489a51186..be5f03745 100644 --- a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java +++ b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java @@ -8,6 +8,7 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; +import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import org.freedesktop.dbus.utils.Util; @@ -31,6 +32,9 @@ public class UnixSocketTransport extends AbstractUnixTransport { UnixSocketTransport(JnrUnixBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); + // resolve dir/tmpdir/runtime (listen side) into a concrete path/abstract; jnr supports abstract sockets + UnixServerAddressResolver.resolve(_address, true); + if (_address.isAbstract()) { unixSocketAddress = new UnixSocketAddress("\0" + _address.getAbstract()); } else if (_address.hasPath()) { diff --git a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java index 7f897c44b..bcae459da 100644 --- a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java +++ b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java @@ -3,6 +3,7 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; +import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import org.newsclub.net.unix.*; @@ -21,6 +22,9 @@ public class JUnixSocketUnixTransport extends AbstractUnixTransport { public JUnixSocketUnixTransport(JUnixSocketBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); + // resolve dir/tmpdir/runtime (listen side) into a concrete path/abstract; use abstract for tmpdir only if the OS supports it + UnixServerAddressResolver.resolve(_address, AFSocket.supports(AFSocketCapability.CAPABILITY_ABSTRACT_NAMESPACE)); + StringBuilder path = new StringBuilder(); if (_address.isAbstract()) { if (!AFSocket.supports(AFSocketCapability.CAPABILITY_ABSTRACT_NAMESPACE)) { diff --git a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java index cd6910946..177c9eaca 100644 --- a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java +++ b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java @@ -3,6 +3,7 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; +import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import java.io.IOException; @@ -37,6 +38,9 @@ public class NativeUnixSocketTransport extends AbstractUnixTransport { NativeUnixSocketTransport(UnixBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); + // resolve dir/tmpdir/runtime (listen side) into a concrete path; native sockets do not support abstract + UnixServerAddressResolver.resolve(_address, false); + if (_address.hasPath()) { unixSocketAddress = UnixDomainSocketAddress.of(_address.getPath()); } else { diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java index ecd952c39..2b1d017b4 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpBusAddress.java @@ -27,6 +27,33 @@ public int getPort() { return Util.isValidNetworkPort(getParameterValue("port"), true) ? Integer.parseInt(getParameterValue("port")) : DEFAULT_PORT; } + public boolean hasFamily() { + return hasParameter("family"); + } + + /** + * The address family requested by the {@code family} parameter ({@code ipv4} or {@code ipv6}), or {@code null}. + * + * @return address family or null + */ + public String getFamily() { + return getParameterValue("family"); + } + + public boolean hasBind() { + return hasParameter("bind"); + } + + /** + * The bind address given by the {@code bind} parameter (listen side only). This may differ from {@link #getHost()} + * (which is the advertised host). The special value {@code *} means "bind to all interfaces". + * + * @return bind address or null + */ + public String getBind() { + return getParameterValue("bind"); + } + /** * Whether this is a {@code nonce-tcp} address (i.e. requires the nonce authentication handshake). * diff --git a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java index 5731a6195..d8ec7d1ed 100644 --- a/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java +++ b/dbus-java-transport-tcp/src/main/java/org/freedesktop/dbus/transport/tcp/TcpTransport.java @@ -7,6 +7,9 @@ import org.freedesktop.dbus.exceptions.AuthenticationException; import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; @@ -16,6 +19,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.security.SecureRandom; import java.util.Arrays; +import java.util.Locale; import java.util.Set; /** @@ -70,10 +74,10 @@ protected void bindImpl() throws IOException { } if (!isBound()) { - InetSocketAddress socketAddress = new InetSocketAddress(getAddress().getHost(), getAddress().getPort()); + InetSocketAddress socketAddress = createBindAddress(); serverSocket = ServerSocketChannel.open(); serverSocket.configureBlocking(true); - getLogger().debug("Binding to {} using local port {}", getAddress().getHost(), + getLogger().debug("Binding to {} using local port {}", socketAddress.getAddress(), getAddress().getPort()); serverSocket.bind(socketAddress); @@ -84,6 +88,27 @@ protected void bindImpl() throws IOException { } } + /** + * Creates the socket address to bind the server socket to, honouring the optional {@code bind} and {@code family} + * address parameters. When {@code bind} is missing, the advertised {@code host} is used; {@code bind=*} binds to all + * interfaces. + * + * @return bind address + * @throws IOException if the host/family cannot be resolved + */ + private InetSocketAddress createBindAddress() throws IOException { + TcpBusAddress address = getAddress(); + int port = address.getPort(); + String bindHost = address.hasBind() ? address.getBind() : address.getHost(); + + if ("*".equals(bindHost)) { + InetAddress wildcard = wildcardAddress(address.getFamily()); + return wildcard == null ? new InetSocketAddress(port) : new InetSocketAddress(wildcard, port); + } + + return new InetSocketAddress(resolveWithFamily(bindHost, address.getFamily()), port); + } + @Override protected SocketChannel acceptImpl() throws IOException { socket = serverSocket.accept(); @@ -101,11 +126,14 @@ protected SocketChannel acceptImpl() throws IOException { @Override public SocketChannel connectImpl() throws IOException { - InetSocketAddress socketAddress = new InetSocketAddress(getAddress().getHost(), getAddress().getPort()); if (getAddress().isListeningSocket()) { throw new IOException("Connect connect to a listening socket (use listenImpl() instead)"); } + InetSocketAddress socketAddress = getAddress().hasFamily() + ? new InetSocketAddress(resolveWithFamily(getAddress().getHost(), getAddress().getFamily()), getAddress().getPort()) + : new InetSocketAddress(getAddress().getHost(), getAddress().getPort()); + socket = SocketChannel.open(); socket.configureBlocking(true); @@ -122,6 +150,52 @@ public SocketChannel connectImpl() throws IOException { return socket; } + /** + * Resolves the given host to an {@link InetAddress} of the requested address family. + * + * @param _host host name or literal IP + * @param _family requested family ({@code ipv4}/{@code ipv6}) or null for no restriction + * + * @return matching address + * @throws IOException if the family is unknown or no matching address exists + */ + private static InetAddress resolveWithFamily(String _host, String _family) throws IOException { + if (_family == null) { + return InetAddress.getByName(_host); + } + + Class wanted = familyClass(_family); + for (InetAddress addr : InetAddress.getAllByName(_host)) { + if (wanted.isInstance(addr)) { + return addr; + } + } + throw new IOException("No " + _family + " address found for host '" + _host + "'"); + } + + /** + * Returns the wildcard ("any local") address for the given family, or null when no family is requested (in which + * case the caller should bind to the family-agnostic wildcard). + */ + private static InetAddress wildcardAddress(String _family) throws IOException { + if (_family == null) { + return null; + } + return switch (_family.toLowerCase(Locale.US)) { + case "ipv4" -> InetAddress.getByName("0.0.0.0"); + case "ipv6" -> InetAddress.getByName("::"); + default -> throw new IOException("Unsupported address family '" + _family + "'"); + }; + } + + private static Class familyClass(String _family) throws IOException { + return switch (_family.toLowerCase(Locale.US)) { + case "ipv4" -> Inet4Address.class; + case "ipv6" -> Inet6Address.class; + default -> throw new IOException("Unsupported address family '" + _family + "'"); + }; + } + /** * Generates a random nonce and writes it to the configured nonce file (server side). * From 7b2bbe76bb7413e02fee8b3ae0c3b4735b2de575 Mon Sep 17 00:00:00 2001 From: David M Date: Sun, 26 Jul 2026 17:10:44 +0200 Subject: [PATCH 32/38] Improved implementation of BusAddress in combination with various unixsocket transports --- .../transports/AbstractUnixBusAddress.java | 110 ++++++++++++++++++ .../transports/UnixServerAddressResolver.java | 80 ------------- .../AbstractUnixBusAddressTest.java | 94 +++++++++++++++ .../UnixServerAddressResolverTest.java | 84 ------------- .../dbus/transport/jnr/JnrUnixBusAddress.java | 35 +----- .../transport/jnr/UnixSocketTransport.java | 4 - .../junixsocket/JUnixSocketBusAddress.java | 37 ++---- .../junixsocket/JUnixSocketUnixTransport.java | 4 - .../jre/NativeUnixSocketTransport.java | 4 - .../dbus/transport/jre/UnixBusAddress.java | 27 +---- 10 files changed, 221 insertions(+), 258 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java delete mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddressTest.java delete mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java new file mode 100644 index 000000000..220db2cbf --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java @@ -0,0 +1,110 @@ +package org.freedesktop.dbus.connections.transports; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; +import org.freedesktop.dbus.utils.Util; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.SecureRandom; +import java.util.Set; + +/** + * Common base class for the unix socket {@link BusAddress} variants of the different transports. + *

+ * Besides the shared {@code path}/{@code abstract} accessors it resolves the listen-side address parameters + * {@code dir}, {@code tmpdir} and {@code runtime} into a concrete {@code path} (or {@code abstract}) as described by + * the D-Bus specification. This resolution happens once, when the (transport-internal) address copy is created, so the + * {@link BusAddress} provided by the caller is never modified. + *

+ * + * @since 6.0.0 + */ +public abstract class AbstractUnixBusAddress extends BusAddress implements IFileBasedBusAddress { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int RANDOM_CHARS = 10; + + /** + * Creates a new unix bus address from the given (already parsed) address and resolves the listen-side + * {@code dir}/{@code tmpdir}/{@code runtime} parameters. + * + * @param _obj source address (copied, never modified) + * @param _supportsAbstract whether the concrete transport supports abstract sockets (relevant for {@code tmpdir}) + * + * @throws TransportConfigurationException if a parameter value is invalid or the environment is incomplete + */ + protected AbstractUnixBusAddress(BusAddress _obj, boolean _supportsAbstract) throws TransportConfigurationException { + super(_obj); + resolveServerAddress(_supportsAbstract); + } + + public boolean hasPath() { + return hasParameter("path"); + } + + public String getPath() { + return getParameterValue("path"); + } + + public boolean isAbstract() { + return hasParameter("abstract"); + } + + public String getAbstract() { + return getParameterValue("abstract"); + } + + @Override + public void updatePermissions(String _fileOwner, String _fileGroup, Set _fileUnixPermissions) { + Util.setFilePermissions(Path.of(getPath()), _fileOwner, _fileGroup, _fileUnixPermissions); + } + + /** + * Resolves the listen-side {@code dir}/{@code tmpdir}/{@code runtime} parameters into a concrete {@code path} or + * {@code abstract} parameter. Only applies to listening addresses that do not already carry a concrete socket. + */ + private void resolveServerAddress(boolean _supportsAbstract) throws TransportConfigurationException { + if (!isListeningSocket() || hasParameter("path") || hasParameter("abstract")) { + return; + } + + if (hasParameter("runtime")) { + String runtime = getParameterValue("runtime"); + if (!"yes".equals(runtime)) { + throw new TransportConfigurationException("unix address parameter 'runtime' only accepts the value 'yes'"); + } + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + if (xdgRuntimeDir == null || xdgRuntimeDir.isBlank()) { + throw new TransportConfigurationException("runtime=yes requires the XDG_RUNTIME_DIR environment variable to be set"); + } + addParameter("path", new File(xdgRuntimeDir, "bus").getAbsolutePath()); + } else if (hasParameter("dir")) { + addParameter("path", randomSocketPath(getParameterValue("dir"))); + } else if (hasParameter("tmpdir")) { + String tmpDir = getParameterValue("tmpdir"); + if (_supportsAbstract) { + addParameter("abstract", randomSocketPath(tmpDir)); + } else { + addParameter("path", randomSocketPath(tmpDir)); + } + } + // no dir/tmpdir/runtime present: leave as-is, the transport will report the missing path/abstract + } + + /** + * Creates a not-yet-existing socket path with a random {@code dbus-XXXXXXXXXX} file name in the given directory. + */ + private static String randomSocketPath(String _dir) { + File file; + do { + StringBuilder sb = new StringBuilder("dbus-"); + for (int i = 0; i < RANDOM_CHARS; i++) { + sb.append((char) (RANDOM.nextInt(26) + 'A')); + } + file = new File(_dir, sb.toString()); + } while (file.exists()); + return file.getAbsolutePath(); + } +} diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java deleted file mode 100644 index 259635a9c..000000000 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolver.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.freedesktop.dbus.connections.transports; - -import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.exceptions.TransportConfigurationException; - -import java.io.File; -import java.security.SecureRandom; - -/** - * Resolves the listen-side unix address parameters {@code dir}, {@code tmpdir} and {@code runtime} into a concrete - * {@code path} (or {@code abstract}) as described by the D-Bus specification. - *

- * These parameters may only be used in server (listening) addresses; the resulting client address will contain the - * concrete {@code path}/{@code abstract} instead. For client addresses (or addresses that already provide a concrete - * {@code path}/{@code abstract}) this resolver does nothing. - *

- * - * @since 6.0.0 - */ -public final class UnixServerAddressResolver { - - private static final SecureRandom RANDOM = new SecureRandom(); - private static final int RANDOM_CHARS = 10; - - private UnixServerAddressResolver() { - } - - /** - * Resolves {@code dir}/{@code tmpdir}/{@code runtime} on the given listening unix address into a concrete - * {@code path} or {@code abstract} parameter (added to the address in place). - * - * @param _address unix bus address - * @param _supportsAbstract whether the transport supports abstract sockets (used for {@code tmpdir}) - * - * @throws TransportConfigurationException if a parameter value is invalid or the environment is incomplete - */ - public static void resolve(BusAddress _address, boolean _supportsAbstract) throws TransportConfigurationException { - // only listening addresses carry dir/tmpdir/runtime, and only when no concrete socket was given - if (!_address.isListeningSocket() || _address.hasParameter("path") || _address.hasParameter("abstract")) { - return; - } - - if (_address.hasParameter("runtime")) { - String runtime = _address.getParameterValue("runtime"); - if (!"yes".equals(runtime)) { - throw new TransportConfigurationException("unix address parameter 'runtime' only accepts the value 'yes'"); - } - String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); - if (xdgRuntimeDir == null || xdgRuntimeDir.isBlank()) { - throw new TransportConfigurationException("runtime=yes requires the XDG_RUNTIME_DIR environment variable to be set"); - } - _address.addParameter("path", new File(xdgRuntimeDir, "bus").getAbsolutePath()); - } else if (_address.hasParameter("dir")) { - _address.addParameter("path", randomSocketPath(_address.getParameterValue("dir"))); - } else if (_address.hasParameter("tmpdir")) { - String tmpDir = _address.getParameterValue("tmpdir"); - if (_supportsAbstract) { - _address.addParameter("abstract", randomSocketPath(tmpDir)); - } else { - _address.addParameter("path", randomSocketPath(tmpDir)); - } - } - // no dir/tmpdir/runtime present: leave as-is, the transport will report the missing path/abstract - } - - /** - * Creates a not-yet-existing socket path with a random {@code dbus-XXXXXXXXXX} file name in the given directory. - */ - private static String randomSocketPath(String _dir) { - File file; - do { - StringBuilder sb = new StringBuilder("dbus-"); - for (int i = 0; i < RANDOM_CHARS; i++) { - sb.append((char) (RANDOM.nextInt(26) + 'A')); - } - file = new File(_dir, sb.toString()); - } while (file.exists()); - return file.getAbsolutePath(); - } -} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddressTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddressTest.java new file mode 100644 index 000000000..4e8b73832 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddressTest.java @@ -0,0 +1,94 @@ +package org.freedesktop.dbus.connections.transports; + +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; + +import java.io.File; + +/** + * Tests the listen-side unix address resolution ({@code dir}/{@code tmpdir}/{@code runtime} -> + * {@code path}/{@code abstract}) implemented in {@link AbstractUnixBusAddress}. + */ +class AbstractUnixBusAddressTest extends AbstractBaseTest { + + private static final String TMP = System.getProperty("java.io.tmpdir"); + + @Test + void testDirResolvesToRandomPath() throws Exception { + TestUnixBusAddress address = new TestUnixBusAddress(BusAddress.of("unix:dir=" + TMP + ",listen=true"), false); + + assertTrue(address.hasPath(), "path should have been resolved"); + assertTrue(address.getPath().startsWith(new File(TMP, "dbus-").getAbsolutePath()), "unexpected path: " + address.getPath()); + assertFalse(address.isAbstract(), "no abstract expected for dir"); + } + + @Test + void testTmpdirUsesAbstractWhenSupported() throws Exception { + TestUnixBusAddress address = new TestUnixBusAddress(BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"), true); + + assertTrue(address.isAbstract(), "abstract should have been resolved for tmpdir when supported"); + assertFalse(address.hasPath(), "no path expected when abstract is used"); + } + + @Test + void testTmpdirUsesPathWhenAbstractUnsupported() throws Exception { + TestUnixBusAddress address = new TestUnixBusAddress(BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"), false); + + assertTrue(address.hasPath(), "path should have been resolved for tmpdir when abstract unsupported"); + assertFalse(address.isAbstract(), "no abstract expected when unsupported"); + } + + @Test + void testRuntimeYes() throws Exception { + BusAddress source = BusAddress.of("unix:runtime=yes,listen=true"); + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + + if (xdgRuntimeDir != null && !xdgRuntimeDir.isBlank()) { + TestUnixBusAddress address = new TestUnixBusAddress(source, false); + assertEquals(new File(xdgRuntimeDir, "bus").getAbsolutePath(), address.getPath()); + } else { + // without XDG_RUNTIME_DIR the resolver must reject runtime=yes + assertThrows(TransportConfigurationException.class, () -> new TestUnixBusAddress(source, false)); + } + } + + @Test + void testRuntimeRejectsInvalidValue() { + BusAddress source = BusAddress.of("unix:runtime=nope,listen=true"); + assertThrows(TransportConfigurationException.class, () -> new TestUnixBusAddress(source, false)); + } + + @Test + void testClientAddressIsNotResolved() throws Exception { + // no listen=true -> client address; dir must be left untouched + TestUnixBusAddress address = new TestUnixBusAddress(BusAddress.of("unix:dir=" + TMP), false); + + assertFalse(address.hasPath(), "client address must not be resolved"); + assertFalse(address.isAbstract(), "client address must not be resolved"); + } + + @Test + void testExistingPathIsKept() throws Exception { + TestUnixBusAddress address = new TestUnixBusAddress(BusAddress.of("unix:path=/tmp/existing.sock,listen=true,dir=" + TMP), false); + + assertEquals("/tmp/existing.sock", address.getPath(), "existing path must be kept"); + } + + @Test + void testSourceAddressIsNotModified() throws Exception { + // resolution happens on the copy: the caller-provided address must remain untouched + BusAddress source = BusAddress.of("unix:dir=" + TMP + ",listen=true"); + new TestUnixBusAddress(source, false); + + assertFalse(source.hasParameter("path"), "source address must not be modified by resolution"); + } + + /** Minimal concrete {@link AbstractUnixBusAddress} to exercise the shared resolution logic. */ + private static final class TestUnixBusAddress extends AbstractUnixBusAddress { + TestUnixBusAddress(BusAddress _obj, boolean _supportsAbstract) throws TransportConfigurationException { + super(_obj, _supportsAbstract); + } + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java deleted file mode 100644 index cf0cb25e3..000000000 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/transports/UnixServerAddressResolverTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.freedesktop.dbus.connections.transports; - -import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.exceptions.TransportConfigurationException; -import org.freedesktop.dbus.test.AbstractBaseTest; -import org.junit.jupiter.api.Test; - -import java.io.File; - -/** - * Unit tests for {@link UnixServerAddressResolver} (A5): resolution of the listen-side unix address parameters - * {@code dir}, {@code tmpdir} and {@code runtime} into a concrete {@code path}/{@code abstract}. - */ -class UnixServerAddressResolverTest extends AbstractBaseTest { - - private static final String TMP = System.getProperty("java.io.tmpdir"); - - @Test - void testDirResolvesToRandomPath() throws Exception { - BusAddress address = BusAddress.of("unix:dir=" + TMP + ",listen=true"); - UnixServerAddressResolver.resolve(address, false); - - assertTrue(address.hasParameter("path"), "path should have been resolved"); - String path = address.getParameterValue("path"); - assertTrue(path.startsWith(new File(TMP, "dbus-").getAbsolutePath()), "unexpected path: " + path); - assertFalse(address.hasParameter("abstract"), "no abstract expected for dir"); - } - - @Test - void testTmpdirUsesAbstractWhenSupported() throws Exception { - BusAddress address = BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"); - UnixServerAddressResolver.resolve(address, true); - - assertTrue(address.hasParameter("abstract"), "abstract should have been resolved for tmpdir when supported"); - assertFalse(address.hasParameter("path"), "no path expected when abstract is used"); - } - - @Test - void testTmpdirUsesPathWhenAbstractUnsupported() throws Exception { - BusAddress address = BusAddress.of("unix:tmpdir=" + TMP + ",listen=true"); - UnixServerAddressResolver.resolve(address, false); - - assertTrue(address.hasParameter("path"), "path should have been resolved for tmpdir when abstract unsupported"); - assertFalse(address.hasParameter("abstract"), "no abstract expected when unsupported"); - } - - @Test - void testRuntimeYes() throws Exception { - BusAddress address = BusAddress.of("unix:runtime=yes,listen=true"); - String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); - - if (xdgRuntimeDir != null && !xdgRuntimeDir.isBlank()) { - UnixServerAddressResolver.resolve(address, false); - assertEquals(new File(xdgRuntimeDir, "bus").getAbsolutePath(), address.getParameterValue("path")); - } else { - // without XDG_RUNTIME_DIR the resolver must reject runtime=yes - assertThrows(TransportConfigurationException.class, () -> UnixServerAddressResolver.resolve(address, false)); - } - } - - @Test - void testRuntimeRejectsInvalidValue() { - BusAddress address = BusAddress.of("unix:runtime=nope,listen=true"); - assertThrows(TransportConfigurationException.class, () -> UnixServerAddressResolver.resolve(address, false)); - } - - @Test - void testClientAddressIsNotResolved() throws Exception { - // no listen=true -> client address; dir must be left untouched - BusAddress address = BusAddress.of("unix:dir=" + TMP); - UnixServerAddressResolver.resolve(address, false); - - assertFalse(address.hasParameter("path"), "client address must not be resolved"); - assertFalse(address.hasParameter("abstract"), "client address must not be resolved"); - } - - @Test - void testExistingPathIsKept() throws Exception { - BusAddress address = BusAddress.of("unix:path=/tmp/existing.sock,listen=true,dir=" + TMP); - UnixServerAddressResolver.resolve(address, false); - - assertEquals("/tmp/existing.sock", address.getParameterValue("path"), "existing path must be kept"); - } -} diff --git a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/JnrUnixBusAddress.java b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/JnrUnixBusAddress.java index 04ef7f038..c0fbcfaf6 100644 --- a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/JnrUnixBusAddress.java +++ b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/JnrUnixBusAddress.java @@ -1,38 +1,13 @@ package org.freedesktop.dbus.transport.jnr; import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.connections.transports.IFileBasedBusAddress; -import org.freedesktop.dbus.utils.Util; +import org.freedesktop.dbus.connections.transports.AbstractUnixBusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Set; +public class JnrUnixBusAddress extends AbstractUnixBusAddress { -public class JnrUnixBusAddress extends BusAddress implements IFileBasedBusAddress { - - public JnrUnixBusAddress(BusAddress _obj) { - super(_obj); - } - - public boolean hasPath() { - return hasParameter("path"); - } - - public String getAbstract() { - return getParameterValue("abstract"); - } - - public boolean isAbstract() { - return hasParameter("abstract"); - } - - public String getPath() { - return getParameterValue("path"); - } - - @Override - public void updatePermissions(String _fileOwner, String _fileGroup, Set _fileUnixPermissions) { - Util.setFilePermissions(Path.of(getPath()), _fileOwner, _fileGroup, _fileUnixPermissions); + public JnrUnixBusAddress(BusAddress _obj) throws TransportConfigurationException { + super(_obj, true); // jnr-unixsocket supports abstract sockets } } diff --git a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java index be5f03745..489a51186 100644 --- a/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java +++ b/dbus-java-transport-jnr-unixsocket/src/main/java/org/freedesktop/dbus/transport/jnr/UnixSocketTransport.java @@ -8,7 +8,6 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; -import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import org.freedesktop.dbus.utils.Util; @@ -32,9 +31,6 @@ public class UnixSocketTransport extends AbstractUnixTransport { UnixSocketTransport(JnrUnixBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); - // resolve dir/tmpdir/runtime (listen side) into a concrete path/abstract; jnr supports abstract sockets - UnixServerAddressResolver.resolve(_address, true); - if (_address.isAbstract()) { unixSocketAddress = new UnixSocketAddress("\0" + _address.getAbstract()); } else if (_address.hasPath()) { diff --git a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketBusAddress.java b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketBusAddress.java index 21ccb31fd..643c38d4d 100644 --- a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketBusAddress.java +++ b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketBusAddress.java @@ -1,38 +1,15 @@ package org.freedesktop.dbus.transport.junixsocket; import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.connections.transports.IFileBasedBusAddress; -import org.freedesktop.dbus.utils.Util; +import org.freedesktop.dbus.connections.transports.AbstractUnixBusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; +import org.newsclub.net.unix.AFSocket; +import org.newsclub.net.unix.AFSocketCapability; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Set; +public class JUnixSocketBusAddress extends AbstractUnixBusAddress { -public class JUnixSocketBusAddress extends BusAddress implements IFileBasedBusAddress { - - public JUnixSocketBusAddress(BusAddress _busAddress) { - super(_busAddress); - } - - public boolean hasPath() { - return hasParameter("path"); - } - - public String getAbstract() { - return getParameterValue("abstract"); - } - - public boolean isAbstract() { - return hasParameter("abstract"); - } - - public Path getPath() { - return Path.of(getParameterValue("path")); - } - - @Override - public void updatePermissions(String _fileOwner, String _fileGroup, Set _fileUnixPermissions) { - Util.setFilePermissions(getPath(), _fileOwner, _fileGroup, _fileUnixPermissions); + public JUnixSocketBusAddress(BusAddress _busAddress) throws TransportConfigurationException { + super(_busAddress, AFSocket.supports(AFSocketCapability.CAPABILITY_ABSTRACT_NAMESPACE)); } } diff --git a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java index bcae459da..7f897c44b 100644 --- a/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java +++ b/dbus-java-transport-junixsocket/src/main/java/org/freedesktop/dbus/transport/junixsocket/JUnixSocketUnixTransport.java @@ -3,7 +3,6 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; -import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import org.newsclub.net.unix.*; @@ -22,9 +21,6 @@ public class JUnixSocketUnixTransport extends AbstractUnixTransport { public JUnixSocketUnixTransport(JUnixSocketBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); - // resolve dir/tmpdir/runtime (listen side) into a concrete path/abstract; use abstract for tmpdir only if the OS supports it - UnixServerAddressResolver.resolve(_address, AFSocket.supports(AFSocketCapability.CAPABILITY_ABSTRACT_NAMESPACE)); - StringBuilder path = new StringBuilder(); if (_address.isAbstract()) { if (!AFSocket.supports(AFSocketCapability.CAPABILITY_ABSTRACT_NAMESPACE)) { diff --git a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java index 177c9eaca..cd6910946 100644 --- a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java +++ b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/NativeUnixSocketTransport.java @@ -3,7 +3,6 @@ import org.freedesktop.dbus.connections.SASL; import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.connections.transports.AbstractUnixTransport; -import org.freedesktop.dbus.connections.transports.UnixServerAddressResolver; import org.freedesktop.dbus.exceptions.TransportConfigurationException; import java.io.IOException; @@ -38,9 +37,6 @@ public class NativeUnixSocketTransport extends AbstractUnixTransport { NativeUnixSocketTransport(UnixBusAddress _address, TransportConfig _config) throws TransportConfigurationException { super(_address, _config); - // resolve dir/tmpdir/runtime (listen side) into a concrete path; native sockets do not support abstract - UnixServerAddressResolver.resolve(_address, false); - if (_address.hasPath()) { unixSocketAddress = UnixDomainSocketAddress.of(_address.getPath()); } else { diff --git a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/UnixBusAddress.java b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/UnixBusAddress.java index f44fecde8..5c1c1df01 100644 --- a/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/UnixBusAddress.java +++ b/dbus-java-transport-native-unixsocket/src/main/java/org/freedesktop/dbus/transport/jre/UnixBusAddress.java @@ -1,30 +1,13 @@ package org.freedesktop.dbus.transport.jre; import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.connections.transports.IFileBasedBusAddress; -import org.freedesktop.dbus.utils.Util; +import org.freedesktop.dbus.connections.transports.AbstractUnixBusAddress; +import org.freedesktop.dbus.exceptions.TransportConfigurationException; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Set; +public class UnixBusAddress extends AbstractUnixBusAddress { -public class UnixBusAddress extends BusAddress implements IFileBasedBusAddress { - - public UnixBusAddress(BusAddress _obj) { - super(_obj); - } - - public boolean hasPath() { - return hasParameter("path"); - } - - public String getPath() { - return getParameterValue("path"); - } - - @Override - public void updatePermissions(String _fileOwner, String _fileGroup, Set _fileUnixPermissions) { - Util.setFilePermissions(Path.of(getPath()), _fileOwner, _fileGroup, _fileUnixPermissions); + public UnixBusAddress(BusAddress _obj) throws TransportConfigurationException { + super(_obj, false); // native unix sockets do not support abstract sockets } } From cf8181c09176dcfc859274478d24b144b8f25a73 Mon Sep 17 00:00:00 2001 From: David M Date: Mon, 27 Jul 2026 14:35:24 +0200 Subject: [PATCH 33/38] Added Dbus "Verbose" interface --- README.md | 1 + .../org/freedesktop/dbus/bin/DBusDaemon.java | 26 +++++++++++++++++- .../transports/AbstractTransport.java | 6 ++--- .../freedesktop/dbus/interfaces/Verbose.java | 27 +++++++++++++++++++ .../freedesktop/dbus/bin/DebugStatsTest.java | 11 +++++++- src/site/markdown/index.md | 21 +++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Verbose.java diff --git a/README.md b/README.md index ebf54c508..91072600e 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ The library will remain open source and MIT licensed and can still be used, fork - Bounded the pending-error queue to avoid unbounded memory growth for unhandled errors - Clean up sender/receiver executor services when the connection fails to establish - Added support for interactive authorization ([#PR313](https://github.com/hypfvieh/dbus-java/issues/313)), thanks to ([unfamiliarS](https://github.com/unfamiliarS) + - Added interfaces `org.freedesktop.DBus.Verbose` and `org.freedesktop.DBus.Debug.Stats` ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java index 6efd20370..e9c1eb8d1 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/bin/DBusDaemon.java @@ -22,6 +22,7 @@ import org.freedesktop.dbus.interfaces.Monitoring; import org.freedesktop.dbus.interfaces.Peer; import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.interfaces.Verbose; import org.freedesktop.dbus.matchrules.DBusMatchRule; import org.freedesktop.dbus.matchrules.MatchRuleParser; import org.freedesktop.dbus.messages.DBusSignal; @@ -510,11 +511,14 @@ void updateThreadName() { } } - public class DBusServer implements DBus, Introspectable, Peer, Monitoring, Properties, Debug.Stats { + public class DBusServer implements DBus, Introspectable, Peer, Monitoring, Properties, Debug.Stats, Verbose { private final String machineId; private ConnectionStruct connStruct; + /** Whether verbose output was enabled via the {@code org.freedesktop.DBus.Verbose} interface. */ + private boolean verbose; + public DBusServer() { machineId = AddressBuilder.createMachineId(); } @@ -840,6 +844,12 @@ public String Introspect() { + + + + + + """; return """ @@ -1147,6 +1157,20 @@ public Map GetAllMatchRules() { return result; } + @Override + public void EnableVerbose() { + requireDebugEnabled(); + verbose = true; + LOGGER.debug("org.freedesktop.DBus.Verbose: verbose output is now enabled={}", verbose); + } + + @Override + public void DisableVerbose() { + requireDebugEnabled(); + verbose = false; + LOGGER.debug("org.freedesktop.DBus.Verbose: verbose output is now enabled={}", verbose); + } + } public class DBusDaemonSenderThread extends Thread { diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java index 570c412ff..b94ec3866 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractTransport.java @@ -52,14 +52,14 @@ public abstract class AbstractTransport implements Closeable { private final Logger logger = LoggerFactory.getLogger(getClass()); private final BusAddress address; - private TransportConnection transportConnection; - private boolean fileDescriptorSupported; - private final long transportId = TRANSPORT_ID_GENERATOR.incrementAndGet(); private final TransportConfig config; private final MessageFactory messageFactory; + private TransportConnection transportConnection; + private boolean fileDescriptorSupported; + protected AbstractTransport(BusAddress _address, TransportConfig _config) { address = Objects.requireNonNull(_address, "BusAddress required"); config = Objects.requireNonNull(_config, "Config required"); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Verbose.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Verbose.java new file mode 100644 index 000000000..abe31cdc3 --- /dev/null +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/interfaces/Verbose.java @@ -0,0 +1,27 @@ +package org.freedesktop.dbus.interfaces; + +import org.freedesktop.dbus.annotations.DBusInterfaceName; + +/** + * The {@code org.freedesktop.DBus.Verbose} interface toggles the verbose (debug) output of the message bus. + *

+ * Like {@link Debug.Stats} this interface is only offered by the reference {@code dbus-daemon} when it was compiled + * with the corresponding debug support. In dbus-java it is only available on a message bus started with debug features + * enabled (see the debug-enabled variant of the embedded daemon); on a default daemon calling these methods results in + * an {@code org.freedesktop.DBus.Error.UnknownMethod} error. + *

+ */ +@DBusInterfaceName("org.freedesktop.DBus.Verbose") +@SuppressWarnings({"checkstyle:methodname"}) +public interface Verbose extends DBusInterface { + + /** + * Enables verbose output on the message bus. + */ + void EnableVerbose(); + + /** + * Disables verbose output on the message bus. + */ + void DisableVerbose(); +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java index 5c43c70cc..fc0ddebb7 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java @@ -8,6 +8,7 @@ import org.freedesktop.dbus.errors.UnknownMethod; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.Debug; +import org.freedesktop.dbus.interfaces.Verbose; import org.freedesktop.dbus.test.AbstractBaseTest; import org.freedesktop.dbus.types.UInt32; import org.freedesktop.dbus.types.Variant; @@ -60,6 +61,11 @@ void testDebugStatsAvailableOnDebuggableDaemon() throws Exception { // unknown connection name -> error assertThrows(ServiceUnknown.class, () -> stats.GetConnectionStats("com.does.not.Exist")); + + // org.freedesktop.DBus.Verbose is available too when debug features are enabled + Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); + assertDoesNotThrow(verbose::EnableVerbose); + assertDoesNotThrow(verbose::DisableVerbose); } } } @@ -76,8 +82,11 @@ void testDebugStatsUnavailableOnDefaultDaemon() throws Exception { try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); - // a default daemon behaves like a production reference daemon: the interface does not exist + // a default daemon behaves like a production reference daemon: the interfaces do not exist assertThrows(UnknownMethod.class, stats::GetStats); + + Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); + assertThrows(UnknownMethod.class, verbose::EnableVerbose); } } } diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index a9cbe9ba9..13a0098a9 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -44,6 +44,27 @@ transport architecture, and the practical use cases are too rare to justify the most common scenario - tunnelling D-Bus over SSH - a separate, third-party transport based on SSHj already exists. +The `launchd:` transport (macOS-only) is not provided either. It merely looks up the actual +session bus socket via macOS' `launchd`; dbus-java already supports this indirectly by honouring +the `DBUS_LAUNCHD_SESSION_BUS_SOCKET` environment variable, so a dedicated transport would add +little. + +### Session bus discovery and `autolaunch` + +When connecting to the session bus, dbus-java resolves the address from (in order) the +`DBUS_SESSION_BUS_ADDRESS` system property, the `DBUS_SESSION_BUS_ADDRESS` environment variable +(on macOS also `DBUS_LAUNCHD_SESSION_BUS_SOCKET`), and finally the classic +`$HOME/.dbus/session-bus/-` session file. If none of these yield an address, +the connection fails. + +The reference implementations additionally support `autolaunch:`, which auto-starts a session bus +daemon on demand (via the external `dbus-launch` helper and an X11 root-window property on Linux, +or a platform-native mechanism on Windows). dbus-java intentionally does not do this: it would +require external helpers / platform-native code outside the scope of a pure-Java library, and +silently spawning a bus daemon is undesirable for a client library. If you need an in-process bus, +start an [`EmbeddedDBusDaemon`](https://github.com/hypfvieh/dbus-java/tree/master/dbus-java-examples) +explicitly. + ## Where to go next * [Quickstart](./quick-start.html) - add the dependencies and open a connection From ec2b8425832c7dfca03c4871e0c180d3464998e6 Mon Sep 17 00:00:00 2001 From: David M Date: Mon, 27 Jul 2026 15:14:24 +0200 Subject: [PATCH 34/38] Added support for address fallback --- README.md | 1 + .../base/AbstractConnectionBase.java | 6 +- .../connections/config/TransportConfig.java | 38 +++++++++ .../config/TransportConfigBuilder.java | 19 ++++- .../impl/DBusConnectionBuilder.java | 32 ++++++- .../impl/DirectConnectionBuilder.java | 26 +++++- .../transports/TransportBuilder.java | 77 +++++++++++++---- .../dbus/utils/AddressBuilder.java | 36 +++++++- .../dbus/connections/ConnectFallbackTest.java | 85 +++++++++++++++++++ 9 files changed, 294 insertions(+), 26 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/ConnectFallbackTest.java diff --git a/README.md b/README.md index 91072600e..b0739022a 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ The library will remain open source and MIT licensed and can still be used, fork - Clean up sender/receiver executor services when the connection fails to establish - Added support for interactive authorization ([#PR313](https://github.com/hypfvieh/dbus-java/issues/313)), thanks to ([unfamiliarS](https://github.com/unfamiliarS) - Added interfaces `org.freedesktop.DBus.Verbose` and `org.freedesktop.DBus.Debug.Stats` + - Added support for address fallback. DBus Specification allow specifying of multiple addresses when connecting. These addresses should be tried in the given order until one is available or all fail. Previously dbus-java only used the first address, ignoring all others. This behavior is now fixed and behaves like the specification defines it ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java index 6d4d4f126..7aa75f079 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/AbstractConnectionBase.java @@ -73,8 +73,6 @@ public abstract sealed class AbstractConnectionBase implements Closeable permits private final Queue pendingErrorQueue; private final AtomicInteger pendingErrorCount = new AtomicInteger(0); - private final BusAddress busAddress; - private final MessageFactory messageFactory; private final ConnectionConfig connectionConfig; @@ -82,6 +80,8 @@ public abstract sealed class AbstractConnectionBase implements Closeable permits private volatile boolean disconnecting; + private BusAddress busAddress; + protected AbstractConnectionBase(ConnectionConfig _conCfg, TransportConfig _transportConfig, ReceivingServiceConfig _rsCfg) throws DBusException { logger = LoggerFactory.getLogger(getClass()); connectionConfig = Objects.requireNonNull(_conCfg, "Connection configuration required"); @@ -122,6 +122,8 @@ protected AbstractConnectionBase(ConnectionConfig _conCfg, TransportConfig _tran try { transport = transportBuilder.build(); + // update to the address that was actually connected to (may differ from the primary when using fallback) + busAddress = transportBuilder.getAddress(); messageFactory = Optional.ofNullable(transport) .map(AbstractTransport::getMessageFactory) .orElseThrow(); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfig.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfig.java index 5da1ea33f..1fe7a7491 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfig.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfig.java @@ -20,8 +20,12 @@ public final class TransportConfig { private SaslConfig saslConfig; + /** Effective bus address (primary candidate, or the address that was actually connected to). */ private BusAddress busAddress; + /** Ordered list of candidate bus addresses to try when connecting (connect-fallback). */ + private List busAddresses = new ArrayList<>(); + private Consumer preConnectCallback; private Consumer afterBindCallback; @@ -52,6 +56,9 @@ public final class TransportConfig { public TransportConfig(BusAddress _address) { busAddress = _address; + if (_address != null) { + busAddresses.add(_address); + } } public TransportConfig() { @@ -62,10 +69,41 @@ public BusAddress getBusAddress() { return busAddress; } + /** + * Sets the effective bus address. Does not modify the candidate list (see {@link #setBusAddresses(List)}); it is + * used both to configure a single address and to record the address that was actually connected to. + * + * @param _busAddress address, never null + */ public void setBusAddress(BusAddress _busAddress) { busAddress = Objects.requireNonNull(_busAddress, "BusAddress required"); } + /** + * Ordered list of candidate bus addresses. When more than one is present, connecting tries them in order until one + * succeeds (connect-fallback). Never null; may be empty if only a single {@link #getBusAddress()} was configured. + * + * @return ordered candidate list + */ + public List getBusAddresses() { + return busAddresses; + } + + /** + * Sets the ordered list of candidate bus addresses and updates the effective {@link #getBusAddress()} to the first + * entry. + * + * @param _busAddresses candidate addresses, never null or empty + */ + public void setBusAddresses(List _busAddresses) { + Objects.requireNonNull(_busAddresses, "BusAddresses required"); + if (_busAddresses.isEmpty()) { + throw new IllegalArgumentException("At least one BusAddress is required"); + } + busAddresses = new ArrayList<>(_busAddresses); + busAddress = busAddresses.getFirst(); + } + public void setListening(boolean _listen) { updateBusAddress(_listen); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfigBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfigBuilder.java index fc07636f0..39a5ab66b 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfigBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/config/TransportConfigBuilder.java @@ -6,6 +6,7 @@ import org.freedesktop.dbus.spi.transport.ITransportProvider; import java.nio.file.attribute.PosixFilePermission; +import java.util.List; import java.util.Objects; import java.util.ServiceLoader; import java.util.function.Consumer; @@ -56,7 +57,23 @@ public X withConfig(TransportConfig _config) { * @return this */ public X withBusAddress(BusAddress _address) { - config.setBusAddress(Objects.requireNonNull(_address, "BusAddress required")); + config.setBusAddresses(List.of(Objects.requireNonNull(_address, "BusAddress required"))); + return self(); + } + + /** + * Set an ordered list of candidate {@link BusAddress}es to use for the connection. When connecting, the addresses + * are tried in order until one succeeds (connect-fallback). The first address becomes the effective/primary + * address. + * + * @param _addresses candidate addresses, never null or empty + * + * @return this + * + * @since 6.0.0 + */ + public X withBusAddresses(List _addresses) { + config.setBusAddresses(Objects.requireNonNull(_addresses, "BusAddresses required")); return self(); } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java index f829d59df..0813b99c2 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java @@ -11,6 +11,7 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.utils.AddressBuilder; +import java.util.List; import java.util.Optional; /** @@ -37,8 +38,11 @@ private DBusConnectionBuilder(BusAddress _address, String _machineId) { * @return {@link DBusConnectionBuilder} */ public static DBusConnectionBuilder forSessionBus(String _machineIdFileLocation) { - BusAddress address = validateTransportAddress(AddressBuilder.getSessionConnection(_machineIdFileLocation)); - return new DBusConnectionBuilder(address, getDbusMachineId(_machineIdFileLocation)); + List addresses = AddressBuilder.getSessionConnectionAddresses(_machineIdFileLocation); + validateTransportAddress(addresses.getFirst()); + DBusConnectionBuilder builder = new DBusConnectionBuilder(addresses.getFirst(), getDbusMachineId(_machineIdFileLocation)); + builder.transportConfig().withBusAddresses(addresses); + return builder; } /** @@ -96,7 +100,29 @@ public static DBusConnectionBuilder forType(DBusBusType _type, String _machineId * @return this */ public static DBusConnectionBuilder forAddress(String _address) { - return new DBusConnectionBuilder(BusAddress.of(_address), getDbusMachineId(null)); + List addresses = BusAddress.parseAll(_address); + DBusConnectionBuilder builder = new DBusConnectionBuilder(addresses.getFirst(), getDbusMachineId(null)); + builder.transportConfig().withBusAddresses(addresses); + return builder; + } + + /** + * Use the given ordered list of addresses to create the connection. When connecting, the addresses are tried in + * order until one succeeds (connect-fallback). + * + * @param _addresses candidate addresses, at least one required + * @return this + * + * @since 6.0.0 + */ + public static DBusConnectionBuilder forAddresses(BusAddress... _addresses) { + if (_addresses == null || _addresses.length == 0) { + throw new IllegalArgumentException("At least one BusAddress is required"); + } + List addresses = List.of(_addresses); + DBusConnectionBuilder builder = new DBusConnectionBuilder(addresses.getFirst(), getDbusMachineId(null)); + builder.transportConfig().withBusAddresses(addresses); + return builder; } /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnectionBuilder.java index 8a315dd5e..b8d9f6926 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DirectConnectionBuilder.java @@ -5,6 +5,8 @@ import org.freedesktop.dbus.connections.config.TransportConfig; import org.freedesktop.dbus.exceptions.DBusException; +import java.util.List; + /** * Builder to create a new DirectConnection. * @@ -24,7 +26,29 @@ private DirectConnectionBuilder(BusAddress _address) { * @return this */ public static DirectConnectionBuilder forAddress(String _address) { - return new DirectConnectionBuilder(BusAddress.of(_address)); + List addresses = BusAddress.parseAll(_address); + DirectConnectionBuilder builder = new DirectConnectionBuilder(addresses.getFirst()); + builder.transportConfig().withBusAddresses(addresses); + return builder; + } + + /** + * Use the given ordered list of addresses to create the connection. When connecting, the addresses are tried in + * order until one succeeds (connect-fallback). + * + * @param _addresses candidate addresses, at least one required + * @return this + * + * @since 6.0.0 + */ + public static DirectConnectionBuilder forAddresses(BusAddress... _addresses) { + if (_addresses == null || _addresses.length == 0) { + throw new IllegalArgumentException("At least one BusAddress is required"); + } + List addresses = List.of(_addresses); + DirectConnectionBuilder builder = new DirectConnectionBuilder(addresses.getFirst()); + builder.transportConfig().withBusAddresses(addresses); + return builder; } /** diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java index 71886d9b8..f63cb058e 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/TransportBuilder.java @@ -9,6 +9,7 @@ import org.freedesktop.dbus.exceptions.TransportConfigurationException; import org.freedesktop.dbus.exceptions.TransportRegistrationException; import org.freedesktop.dbus.spi.transport.ITransportProvider; +import org.freedesktop.dbus.utils.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -178,32 +179,73 @@ public TransportConfigBuilder, Trans * failed */ public AbstractTransport build() throws DBusException, IOException { - BusAddress myBusAddress = getAddress(); TransportConfig config = transportConfigBuilder.build(); - if (myBusAddress == null) { + + List candidates = new ArrayList<>(config.getBusAddresses()); + if (candidates.isEmpty() && config.getBusAddress() != null) { + candidates.add(config.getBusAddress()); + } + if (candidates.isEmpty()) { throw new DBusException("Transport requires a BusAddress, use withBusAddress() to configure before building"); } - int configuredSaslAuthMode = config.getSaslConfig().getAuthMode(); + // connect-fallback only applies to client connections; a listening (server) transport binds a single address + boolean clientConnect = config.isAutoConnect() && !candidates.getFirst().isListeningSocket(); + if (!clientConnect || candidates.size() == 1) { + return buildSingle(candidates.getFirst(), config); + } + + // try each candidate address in order, first successful connection wins + List failures = new ArrayList<>(); + for (BusAddress candidate : candidates) { + try { + return buildSingle(candidate, config); + } catch (IOException | DBusException _ex) { + LOGGER.debug("Could not connect to address {}, trying next candidate", candidate, _ex); + failures.add(_ex); + } + } + + DBusException ex = new DBusException("Unable to connect to any of the configured addresses: " + candidates); + failures.forEach(ex::addSuppressed); + throw ex; + } + + /** + * Creates (and, for client connections, connects) a transport for a single bus address. The given address becomes + * the effective address of the config (so {@link #getAddress()} reflects the address that was actually used). + * + * @param _busAddress address to build the transport for + * @param _config transport configuration + * + * @return connected/created transport + * + * @throws DBusException on configuration errors + * @throws IOException when connecting fails + */ + private AbstractTransport buildSingle(BusAddress _busAddress, TransportConfig _config) throws DBusException, IOException { + _config.setBusAddress(_busAddress); + + int configuredSaslAuthMode = _config.getSaslConfig().getAuthMode(); AbstractTransport transport = null; ITransportProvider provider = PROVIDERS.values().stream() - .map(e -> e.get(config.getBusAddress().getBusType())) + .map(e -> e.get(_busAddress.getBusType())) .filter(Objects::nonNull) .findAny().orElse(null); if (provider == null) { - throw new DBusException("No transport provider found for bustype " + config.getBusAddress().getBusType()); + throw new DBusException("No transport provider found for bustype " + _busAddress.getBusType()); } else { - LOGGER.info("Using transport {} for address {}", provider.getTransportName(), config.getBusAddress()); + LOGGER.info("Using transport {} for address {}", provider.getTransportName(), _busAddress); } try { - transport = provider.createTransport(myBusAddress, config); + transport = provider.createTransport(_busAddress, _config); Objects.requireNonNull(transport, "Transport required"); // in case the factory returns null, we cannot continue // another authentication algorithm was configured manually - if (configuredSaslAuthMode > 0 && config.getSaslConfig().getAuthMode() != configuredSaslAuthMode) { + if (configuredSaslAuthMode > 0 && _config.getSaslConfig().getAuthMode() != configuredSaslAuthMode) { transport.getSaslConfig().setAuthMode(configuredSaslAuthMode); } @@ -212,19 +254,19 @@ public AbstractTransport build() throws DBusException, IOException { } if (transport == null) { - throw new DBusException("Unknown address type " + myBusAddress.getType() + " or no transport provider found for bus type " + myBusAddress.getBusType()); + throw new DBusException("Unknown address type " + _busAddress.getType() + " or no transport provider found for bus type " + _busAddress.getBusType()); } - if (myBusAddress.isListeningSocket() && myBusAddress instanceof IFileBasedBusAddress fbba) { - fbba.updatePermissions(config.getFileOwner(), config.getFileGroup(), config.getFileUnixPermissions()); + if (_busAddress.isListeningSocket() && _busAddress instanceof IFileBasedBusAddress fbba) { + fbba.updatePermissions(_config.getFileOwner(), _config.getFileGroup(), _config.getFileUnixPermissions()); } - transport.setPreConnectCallback(config.getPreConnectCallback()); + transport.setPreConnectCallback(_config.getPreConnectCallback()); - if (config.isAutoConnect() && !config.isListening()) { + if (_config.isAutoConnect() && !_busAddress.isListeningSocket()) { SocketChannel c = null; // support multiple retries so concurrent server/client connection may work out of the box - int max = Math.max(500, config.getTimeout()) / 500; + int max = Math.max(500, _config.getTimeout()) / 500; int cnt = 0; do { try { @@ -232,21 +274,22 @@ public AbstractTransport build() throws DBusException, IOException { c = transport.connect(); } catch (IOException _ex) { // jnr uses IOException when socket address not found, native unix sockets // use ConnectException - LOGGER.debug("Connection to {} failed, reconnect attempt {} of {}", getAddress(), cnt, max); + LOGGER.debug("Connection to {} failed, reconnect attempt {} of {}", _busAddress, cnt, max); if (cnt >= max) { + Util.closeQuietly(transport); throw _ex; } try { Thread.sleep(500); } catch (InterruptedException _ex1) { - LOGGER.debug("Interrupted while waiting for connection retry for address {}", getAddress()); + LOGGER.debug("Interrupted while waiting for connection retry for address {}", _busAddress); Thread.currentThread().interrupt(); } } } while (c == null); - LOGGER.debug("Connection to {} established after {} of {} attempts", getAddress(), cnt, max); + LOGGER.debug("Connection to {} established after {} of {} attempts", _busAddress, cnt, max); } return transport; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java index ccbd4b72b..1d9f1f0fa 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/utils/AddressBuilder.java @@ -40,6 +40,38 @@ public static BusAddress getSystemConnection() { * @throws AddressResolvingException when no suitable address can be found for any available transport */ public static BusAddress getSessionConnection(String _dbusMachineIdFile) { + return BusAddress.of(resolveSessionAddress(_dbusMachineIdFile)); + } + + /** + * Like {@link #getSessionConnection(String)}, but returns all addresses found in the resolved session bus address. + *

+ * The {@code DBUS_SESSION_BUS_ADDRESS} may contain several {@code ;}-separated addresses; the returned list keeps + * their order so callers can use them for connect-fallback. + *

+ * + * @param _dbusMachineIdFile alternative location of dbus machine id file, use null if not needed + * + * @return ordered list of candidate addresses (never empty) + * + * @throws AddressResolvingException when no suitable address can be found + * + * @since 6.0.0 + */ + public static List getSessionConnectionAddresses(String _dbusMachineIdFile) { + return BusAddress.parseAll(resolveSessionAddress(_dbusMachineIdFile)); + } + + /** + * Resolves the raw session bus address string from process properties, environment or the session properties file. + * + * @param _dbusMachineIdFile alternative location of dbus machine id file, use null if not needed + * + * @return raw address string (may contain a {@code ;}-separated list) + * + * @throws AddressResolvingException when no suitable address can be found + */ + private static String resolveSessionAddress(String _dbusMachineIdFile) { // try to read session address from running process instance properties first String s = System.getProperty(DBusSysProps.DBUS_SESSION_BUS_ADDRESS); @@ -91,10 +123,10 @@ public static BusAddress getSessionConnection(String _dbusMachineIdFile) { sessionAddress = sessionAddress.replaceFirst("^'([^']+)'$", "$1"); } - return BusAddress.of(sessionAddress); + return sessionAddress; } - return BusAddress.of(s); + return s; } /** diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/ConnectFallbackTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/ConnectFallbackTest.java new file mode 100644 index 000000000..5a0d61aca --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/connections/ConnectFallbackTest.java @@ -0,0 +1,85 @@ +package org.freedesktop.dbus.connections; + +import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.test.AbstractBaseTest; +import org.junit.jupiter.api.Test; + +/** + * Verifies the connect-fallback over multiple bus addresses (A1b): the addresses are tried in order until one + * connects; if none connects, the build fails. + */ +class ConnectFallbackTest extends AbstractBaseTest { + + /** A fresh, unused endpoint of the active transport (nothing is listening there). */ + private static String deadAddress() { + return TransportBuilder.createDynamicSession(TransportBuilder.getRegisteredBusTypes().getFirst(), false); + } + + @Test + void testFallbackToSecondAddress() throws Exception { + String type = TransportBuilder.getRegisteredBusTypes().getFirst(); + String liveBase = TransportBuilder.createDynamicSession(type, false); + BusAddress dead = BusAddress.of(deadAddress()); + BusAddress live = BusAddress.of(liveBase); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(BusAddress.of(liveBase + ",listen=true"))) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddresses(dead, live) + .transportConfig().withTimeout(1000).back() + .withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "should have connected via the second (live) address"); + } + } + } + + @Test + void testFallbackViaSemicolonSeparatedString() throws Exception { + String type = TransportBuilder.getRegisteredBusTypes().getFirst(); + String liveBase = TransportBuilder.createDynamicSession(type, false); + String combined = deadAddress() + ";" + liveBase; + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(BusAddress.of(liveBase + ",listen=true"))) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddress(combined) + .transportConfig().withTimeout(1000).back() + .withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "should have connected via the live address of the list"); + } + } + } + + @Test + void testAllAddressesFail() { + BusAddress dead1 = BusAddress.of(deadAddress()); + BusAddress dead2 = BusAddress.of(deadAddress()); + + assertThrows(DBusException.class, () -> { + try (DBusConnection conn = DBusConnectionBuilder.forAddresses(dead1, dead2) + .transportConfig().withTimeout(1000).back() + .withShared(false).build()) { + fail("connection must fail when no address is reachable"); + } + }); + } + + @Test + void testSingleAddressStillWorks() throws Exception { + String type = TransportBuilder.getRegisteredBusTypes().getFirst(); + String liveBase = TransportBuilder.createDynamicSession(type, false); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(BusAddress.of(liveBase + ",listen=true"))) { + daemon.startInBackgroundAndWait(MAX_WAIT); + + try (DBusConnection conn = DBusConnectionBuilder.forAddresses(BusAddress.of(liveBase)) + .withShared(false).build()) { + assertNotNull(conn.getUniqueName(), "single-address connect should still work"); + } + } + } +} From af91ff8cd52eb1a98be653c17e258dfdaefb1bf9 Mon Sep 17 00:00:00 2001 From: David M Date: Mon, 27 Jul 2026 15:57:06 +0200 Subject: [PATCH 35/38] Improved tests --- README.md | 1 + .../dbus/bin/BusInterfaceExtrasTest.java | 37 +----- .../freedesktop/dbus/bin/DebugStatsTest.java | 115 ++++++++---------- .../dbus/messages/MessageTest.java | 12 +- .../dbus/test/AbstractBaseTest.java | 21 ++++ .../dbus/test/AbstractDBusBaseTest.java | 36 ++++-- .../dbus/test/AbstractEmbeddedDaemonTest.java | 59 +++++++++ .../freedesktop/dbus/test/HandlerTest.java | 19 +-- .../freedesktop/dbus/test/MonitorTest.java | 42 +++++++ .../freedesktop/dbus/test/SignalNameTest.java | 65 +++++----- .../handler/GenericHandlerWithDecode.java | 9 ++ .../transport/tcp/NonceTcpTransportTest.java | 4 +- .../dbus/transport/tcp/TcpFamilyBindTest.java | 4 +- 13 files changed, 276 insertions(+), 148 deletions(-) create mode 100644 dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractEmbeddedDaemonTest.java diff --git a/README.md b/README.md index b0739022a..05721eb08 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ The library will remain open source and MIT licensed and can still be used, fork - Added support for interactive authorization ([#PR313](https://github.com/hypfvieh/dbus-java/issues/313)), thanks to ([unfamiliarS](https://github.com/unfamiliarS) - Added interfaces `org.freedesktop.DBus.Verbose` and `org.freedesktop.DBus.Debug.Stats` - Added support for address fallback. DBus Specification allow specifying of multiple addresses when connecting. These addresses should be tried in the given order until one is available or all fail. Previously dbus-java only used the first address, ignoring all others. This behavior is now fixed and behaves like the specification defines it + - Improved tests ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java index da7878add..879e7d99f 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/BusInterfaceExtrasTest.java @@ -1,19 +1,14 @@ package org.freedesktop.dbus.bin; -import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.connections.transports.TransportBuilder; import org.freedesktop.dbus.errors.PropertyReadOnly; import org.freedesktop.dbus.errors.UnknownProperty; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.Properties; -import org.freedesktop.dbus.test.AbstractBaseTest; +import org.freedesktop.dbus.test.AbstractEmbeddedDaemonTest; import org.freedesktop.dbus.types.Variant; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -27,14 +22,14 @@ *
  • D5 - the bus properties {@code Features} and {@code Interfaces}
  • * */ -class BusInterfaceExtrasTest extends AbstractBaseTest { +class BusInterfaceExtrasTest extends AbstractEmbeddedDaemonTest { private static final String DBUS_BUSNAME = "org.freedesktop.DBus"; private static final String DBUS_BUSPATH = "/org/freedesktop/DBus"; @Test void testBusInterfaceExtrasOnDefaultDaemon() throws Exception { - withDaemon(false, conn -> { + withEmbeddedConnection(conn -> { // D3 + D5: the introspection data declares the new signal, the Properties interface and the properties Introspectable intro = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Introspectable.class); String xml = intro.Introspect(); @@ -68,7 +63,7 @@ void testBusInterfaceExtrasOnDefaultDaemon() throws Exception { @Test void testInterfacesPropertyListsDebugStatsOnDebuggableDaemon() throws Exception { - withDaemon(true, conn -> { + withEmbeddedConnection(DebuggableEmbeddedDBusDaemon::new, conn -> { Properties props = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Properties.class); List interfaces = asStringList(props.Get(DBUS_BUSNAME, "Interfaces")); assertTrue(interfaces.contains("org.freedesktop.DBus.Debug.Stats"), @@ -94,28 +89,4 @@ private static List asStringList(Object _value) { } return result; } - - private void withDaemon(boolean _debug, ConnectionConsumer _consumer) throws IOException { - String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); - String newAddress = TransportBuilder.createDynamicSession(protocolType, false); - BusAddress busAddress = BusAddress.of(newAddress); - BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); - - try (EmbeddedDBusDaemon daemon = _debug - ? new DebuggableEmbeddedDBusDaemon(listenBusAddress) - : new EmbeddedDBusDaemon(listenBusAddress)) { - daemon.startInBackgroundAndWait(MAX_WAIT); - - try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { - _consumer.accept(conn); - } catch (Exception _ex) { - fail(_ex); - } - } - } - - @FunctionalInterface - private interface ConnectionConsumer { - void accept(DBusConnection _conn) throws Exception; - } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java index fc0ddebb7..8bcce03b9 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/bin/DebugStatsTest.java @@ -1,15 +1,12 @@ package org.freedesktop.dbus.bin; -import org.freedesktop.dbus.connections.BusAddress; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.connections.transports.TransportBuilder; import org.freedesktop.dbus.errors.ServiceUnknown; import org.freedesktop.dbus.errors.UnknownMethod; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.interfaces.Debug; +import org.freedesktop.dbus.interfaces.Introspectable; import org.freedesktop.dbus.interfaces.Verbose; -import org.freedesktop.dbus.test.AbstractBaseTest; +import org.freedesktop.dbus.test.AbstractEmbeddedDaemonTest; import org.freedesktop.dbus.types.UInt32; import org.freedesktop.dbus.types.Variant; import org.junit.jupiter.api.Test; @@ -21,73 +18,65 @@ * Verifies the {@code org.freedesktop.DBus.Debug.Stats} interface which is only offered by the * {@link DebuggableEmbeddedDBusDaemon}, not by the default {@link EmbeddedDBusDaemon}. */ -class DebugStatsTest extends AbstractBaseTest { +class DebugStatsTest extends AbstractEmbeddedDaemonTest { private static final String DBUS_BUSNAME = "org.freedesktop.DBus"; private static final String DBUS_BUSPATH = "/org/freedesktop/DBus"; @Test void testDebugStatsAvailableOnDebuggableDaemon() throws Exception { - String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); - String newAddress = TransportBuilder.createDynamicSession(protocolType, false); - BusAddress busAddress = BusAddress.of(newAddress); - BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); - - try (DebuggableEmbeddedDBusDaemon daemon = new DebuggableEmbeddedDBusDaemon(listenBusAddress)) { - daemon.startInBackgroundAndWait(MAX_WAIT); - - try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { - Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); - - // global statistics contain the documented keys - Map> global = stats.GetStats(); - assertTrue(global.containsKey("ActiveConnections"), "ActiveConnections missing"); - assertTrue(global.containsKey("BusNames"), "BusNames missing"); - assertTrue(global.containsKey("MatchRules"), "MatchRules missing"); - assertInstanceOf(UInt32.class, global.get("ActiveConnections").getValue()); - - // add a match rule and ensure it is reported by GetAllMatchRules - DBus dbus = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, DBus.class); - dbus.AddMatch("type='signal',interface='com.example.Foo'"); - - Map allRules = stats.GetAllMatchRules(); - boolean found = allRules.values().stream().flatMap(Arrays::stream) - .anyMatch(r -> r.contains("com.example.Foo")); - assertTrue(found, "added match rule should be listed by GetAllMatchRules"); - - // per-connection statistics for our own unique name - Map> connStats = stats.GetConnectionStats(conn.getUniqueName()); - assertEquals(conn.getUniqueName(), connStats.get("UniqueName").getValue()); - - // unknown connection name -> error - assertThrows(ServiceUnknown.class, () -> stats.GetConnectionStats("com.does.not.Exist")); - - // org.freedesktop.DBus.Verbose is available too when debug features are enabled - Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); - assertDoesNotThrow(verbose::EnableVerbose); - assertDoesNotThrow(verbose::DisableVerbose); - } - } + withEmbeddedConnection(DebuggableEmbeddedDBusDaemon::new, conn -> { + Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); + + // global statistics contain the documented keys + Map> global = stats.GetStats(); + assertTrue(global.containsKey("ActiveConnections"), "ActiveConnections missing"); + assertTrue(global.containsKey("BusNames"), "BusNames missing"); + assertTrue(global.containsKey("MatchRules"), "MatchRules missing"); + assertInstanceOf(UInt32.class, global.get("ActiveConnections").getValue()); + + // add a match rule and ensure it is reported by GetAllMatchRules + DBus dbus = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, DBus.class); + dbus.AddMatch("type='signal',interface='com.example.Foo'"); + + Map allRules = stats.GetAllMatchRules(); + boolean found = allRules.values().stream().flatMap(Arrays::stream) + .anyMatch(r -> r.contains("com.example.Foo")); + assertTrue(found, "added match rule should be listed by GetAllMatchRules"); + + // per-connection statistics for our own unique name + Map> connStats = stats.GetConnectionStats(conn.getUniqueName()); + assertEquals(conn.getUniqueName(), connStats.get("UniqueName").getValue()); + + // unknown connection name -> error + assertThrows(ServiceUnknown.class, () -> stats.GetConnectionStats("com.does.not.Exist")); + + // org.freedesktop.DBus.Verbose is available too when debug features are enabled ... + Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); + assertDoesNotThrow(verbose::EnableVerbose); + assertDoesNotThrow(verbose::DisableVerbose); + + // ... and is advertised via introspection + Introspectable intro = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Introspectable.class); + assertTrue(intro.Introspect().contains("org.freedesktop.DBus.Verbose"), + "Verbose interface should be introspected on a debuggable daemon"); + }); } @Test void testDebugStatsUnavailableOnDefaultDaemon() throws Exception { - String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); - String newAddress = TransportBuilder.createDynamicSession(protocolType, false); - BusAddress busAddress = BusAddress.of(newAddress); - BusAddress listenBusAddress = BusAddress.of(newAddress + ",listen=true"); - - try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenBusAddress)) { - daemon.startInBackgroundAndWait(MAX_WAIT); - - try (DBusConnection conn = DBusConnectionBuilder.forAddress(busAddress).withShared(false).build()) { - Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); - // a default daemon behaves like a production reference daemon: the interfaces do not exist - assertThrows(UnknownMethod.class, stats::GetStats); - - Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); - assertThrows(UnknownMethod.class, verbose::EnableVerbose); - } - } + withEmbeddedConnection(conn -> { + Debug.Stats stats = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Debug.Stats.class); + // a default daemon behaves like a production reference daemon: the interfaces do not exist + assertThrows(UnknownMethod.class, stats::GetStats); + + Verbose verbose = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Verbose.class); + assertThrows(UnknownMethod.class, verbose::EnableVerbose); + + // the default daemon must not advertise the Verbose interface via introspection either + Introspectable intro = conn.getRemoteObject(DBUS_BUSNAME, DBUS_BUSPATH, Introspectable.class); + assertFalse(intro.Introspect().contains("org.freedesktop.DBus.Verbose"), + "Verbose interface must not be introspected on a default daemon"); + }); } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java index 4d5054d92..25b81ecdd 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/messages/MessageTest.java @@ -60,7 +60,7 @@ public void testReadMessageHeader() throws Exception { } @Test - void testPopulateIgnoresUnknownHeaderField() { + void testPopulateIgnoresUnknownHeaderField() throws Exception { byte[] msg = {108, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0}; // valid header field array, but the first field code (index 8) @@ -74,7 +74,15 @@ void testPopulateIgnoresUnknownHeaderField() { }; byte[] body = {}; - assertDoesNotThrow(() -> new Message().populate(msg, headers, body, null)); + Message m = new Message(); + m.populate(msg, headers, body, null); + + // the unknown field (code 10, originally DESTINATION) must be skipped ... + assertNull(m.getDestination(), "unknown header field must be ignored"); + // ... while all remaining valid fields are still parsed (a regression dropping fields would fail here) + assertEquals(1L, m.getReplySerial(), "reply serial (field 5) should still be parsed"); + assertEquals("s", m.getSig(), "signature (field 8) should still be parsed"); + assertEquals("org.freedesktop.DBus", m.getSource(), "sender (field 7) should still be parsed"); } @Test diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractBaseTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractBaseTest.java index b52e7bd43..ed2721083 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractBaseTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractBaseTest.java @@ -10,6 +10,7 @@ import java.lang.reflect.Method; import java.time.Duration; import java.util.Optional; +import java.util.function.BooleanSupplier; /** * Base test class providing logger and common methods. @@ -55,6 +56,26 @@ public final void logTestEnd(TestInfo _testInfo) { logTestBeginEnd("END", _testInfo); } + /** + * Waits until the given condition becomes {@code true} or the timeout elapses, polling frequently. + *

    + * This is an event-driven replacement for a fixed {@code Thread.sleep(...)}: it returns as soon as the condition + * holds (fast on fast machines) but tolerates up to {@code _timeoutMillis} on slow systems / CI. It never shortens + * the effective wait compared to a fixed sleep — the timeout should be chosen generously (e.g. {@link #MAX_WAIT}). + * The condition is expected to be backed by state updated from another thread (signal/callback handlers). + * + * @param _condition condition to wait for + * @param _timeoutMillis maximum time to wait in milliseconds + * + * @throws InterruptedException if interrupted while waiting + */ + protected static void waitForCondition(BooleanSupplier _condition, long _timeoutMillis) throws InterruptedException { + long deadline = System.currentTimeMillis() + _timeoutMillis; + while (!_condition.getAsBoolean() && System.currentTimeMillis() < deadline) { + Thread.sleep(20L); + } + } + protected void logTestBeginEnd(String _prefix, TestInfo _testInfo) { if (_testInfo.getTestMethod().isEmpty() || _testInfo.getDisplayName().startsWith(_testInfo.getTestMethod().get().getName())) { logger.info(">>>>>>>>>> {} Test: {} <<<<<<<<<<", _prefix, _testInfo.getDisplayName()); diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractDBusBaseTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractDBusBaseTest.java index a1f207422..42ca61f8a 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractDBusBaseTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractDBusBaseTest.java @@ -37,20 +37,32 @@ public void setUp() throws DBusException { @AfterEach public void tearDown() throws Exception { logger.debug("Checking for outstanding errors"); - DBusExecutionException dbee = serverconn.getError(); - if (null != dbee) { - throw dbee; - } - dbee = clientconn.getError(); - if (null != dbee) { - throw dbee; - } + // capture any outstanding errors first, but report them only after both connections were cleaned up, + // so a pending error can never leave a connection connected / the bus name still owned + DBusExecutionException serverError = serverconn == null ? null : serverconn.getError(); + DBusExecutionException clientError = clientconn == null ? null : clientconn.getError(); logger.debug("Disconnecting"); - /** Disconnect from the bus. */ - clientconn.disconnect(); - serverconn.releaseBusName(getTestBusName()); - serverconn.disconnect(); + try { + if (clientconn != null) { + clientconn.disconnect(); + } + } finally { + if (serverconn != null) { + try { + serverconn.releaseBusName(getTestBusName()); + } finally { + serverconn.disconnect(); + } + } + } + + if (serverError != null) { + throw serverError; + } + if (clientError != null) { + throw clientError; + } } protected String getTestObjectPath() { diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractEmbeddedDaemonTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractEmbeddedDaemonTest.java new file mode 100644 index 000000000..9841fc3e0 --- /dev/null +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/AbstractEmbeddedDaemonTest.java @@ -0,0 +1,59 @@ +package org.freedesktop.dbus.test; + +import org.freedesktop.dbus.bin.EmbeddedDBusDaemon; +import org.freedesktop.dbus.connections.BusAddress; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.connections.transports.TransportBuilder; + +import java.util.function.Function; + +/** + * Base class for tests that need a fresh {@link EmbeddedDBusDaemon} on the active transport plus a client connection. + *

    + * Removes the "create dynamic session → start embedded daemon → connect" boilerplate that was duplicated across many + * transport/daemon tests. Tests with special address requirements (address lists, nonce files, custom bind/family, + * {@code dir=} listen addresses) intentionally do not use this helper. + *

    + */ +public abstract class AbstractEmbeddedDaemonTest extends AbstractBaseTest { + + /** + * Starts a default {@link EmbeddedDBusDaemon} on a fresh dynamic session of the active transport, opens a + * non-shared client connection to it and passes it to the given consumer. + * + * @param _consumer receives the connected client connection + * @throws Exception on any failure + */ + protected void withEmbeddedConnection(ConnectionConsumer _consumer) throws Exception { + withEmbeddedConnection(EmbeddedDBusDaemon::new, _consumer); + } + + /** + * Same as {@link #withEmbeddedConnection(ConnectionConsumer)}, but the daemon is created via the given factory + * (e.g. {@code DebuggableEmbeddedDBusDaemon::new}). + * + * @param _daemonFactory factory creating the daemon for a listening bus address + * @param _consumer receives the connected client connection + * @throws Exception on any failure + */ + protected void withEmbeddedConnection(Function _daemonFactory, + ConnectionConsumer _consumer) throws Exception { + String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); + String newAddress = TransportBuilder.createDynamicSession(protocolType, false); + BusAddress connectAddress = BusAddress.of(newAddress); + BusAddress listenAddress = BusAddress.of(newAddress + ",listen=true"); + + try (EmbeddedDBusDaemon daemon = _daemonFactory.apply(listenAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress).withShared(false).build()) { + _consumer.accept(conn); + } + } + } + + @FunctionalInterface + protected interface ConnectionConsumer { + void accept(DBusConnection _conn) throws Exception; + } +} diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/HandlerTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/HandlerTest.java index 4d63574c7..b43dda6c9 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/HandlerTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/HandlerTest.java @@ -99,8 +99,11 @@ public void testSignalHandlers() throws DBusException, InterruptedException { logger.debug("Sending Enum Signal..."); serverconn.sendMessage(new TestEnumSignal(getTestObjectPath(), TestEnum.TESTVAL1, Arrays.asList(TestEnum.TESTVAL2, TestEnum.TESTVAL3))); - // wait some time to receive signals - Thread.sleep(1000L); + // wait (up to a generous timeout) until all signals have been received + waitForCondition(() -> + sigh.getActualTestRuns() == 1 && esh.getActualTestRuns() == 1 && rsh.getActualTestRuns() == 1 + && ash.getActualTestRuns() == 1 && ensh.getActualTestRuns() == 1 + && psh.getActualTestRuns() == 1 && osh.getActualTestRuns() == 1, MAX_WAIT); // ensure callback has been fired at least once assertEquals(1, sigh.getActualTestRuns(), "SignalHandler should have been called"); @@ -146,8 +149,8 @@ public void testGenericSignalHandler() throws DBusException, InterruptedExceptio serverconn.sendMessage(signalToSend); - // wait some time to receive signals - Thread.sleep(1000L); + // wait (up to a generous timeout) until the signal has been received + waitForCondition(() -> genericHandler.getActualTestRuns() == 1, MAX_WAIT); // ensure callback has been fired at least once assertEquals(1, genericHandler.getActualTestRuns(), "GenericHandler should have been called"); @@ -171,8 +174,8 @@ public void testGenericDecodeSignalHandler() throws DBusException, InterruptedEx serverconn.sendMessage(signalToSend); - // wait some time to receive signals - Thread.sleep(1000L); + // wait (up to a generous timeout) until the signal has been received + waitForCondition(genericDecode::hasReceived, MAX_WAIT); assertDoesNotThrow(() -> { genericDecode.incomingSameAsExpected(); @@ -196,8 +199,8 @@ public void testGenericHandlerWithNoInterface() throws DBusException, Interrupte serverconn.sendMessage(signalToSend); - // wait some time to receive signals - Thread.sleep(1000L); + // wait (up to a generous timeout) until the signal has been received + waitForCondition(genericDecode::hasReceived, MAX_WAIT); assertDoesNotThrow(() -> { genericDecode.incomingSameAsExpected(); diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java index b1d8806db..70f369ad6 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/MonitorTest.java @@ -5,8 +5,11 @@ import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.matchrules.DBusMatchRule; +import org.freedesktop.dbus.matchrules.DBusMatchRuleBuilder; import org.freedesktop.dbus.messages.DBusSignal; import org.freedesktop.dbus.messages.Message; +import org.freedesktop.dbus.messages.constants.MessageTypes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -53,6 +56,39 @@ void testMonitorReceivesBusTraffic() throws Exception { } } + @Test + @Timeout(value = 20, unit = TimeUnit.SECONDS) + void testMonitorFiltersByMatchRule() throws Exception { + BlockingQueue received = new LinkedBlockingQueue<>(); + CountDownLatch pingLatch = new CountDownLatch(1); + + // only messages matching this rule (the PingSignal member) must reach the monitor + DBusMatchRule rule = DBusMatchRuleBuilder.create() + .withType(MessageTypes.SIGNAL) + .withInterface("org.freedesktop.dbus.test.MonitorTestSignals") + .withMember("PingSignal") + .build(); + + try (DBusConnection monitorConn = DBusConnectionBuilder.forSessionBus().withShared(false).build()) { + monitorConn.becomeMonitor(List.of(rule), msg -> { + received.add(msg); + if ("PingSignal".equals(msg.getName())) { + pingLatch.countDown(); + } + }); + + // send the non-matching signal FIRST, the matching one SECOND: once the (later) PingSignal has + // been observed, a matching PongSignal would already have been delivered too - so its absence is + // a reliable proof of filtering without relying on a fixed sleep + serverconn.sendMessage(new MonitorTestSignals.PongSignal(getTestObjectPath(), "should-be-filtered")); + serverconn.sendMessage(new MonitorTestSignals.PingSignal(getTestObjectPath(), "hello-monitor")); + + assertTrue(pingLatch.await(15, TimeUnit.SECONDS), "monitor did not receive the matching signal"); + assertTrue(received.stream().noneMatch(m -> "PongSignal".equals(m.getName())), + "monitor must not receive signals that do not match its match rule"); + } + } + @DBusInterfaceName("org.freedesktop.dbus.test.MonitorTestSignals") public interface MonitorTestSignals extends DBusInterface { class PingSignal extends DBusSignal { @@ -60,5 +96,11 @@ public PingSignal(String _path, String _value) throws DBusException { super(_path, _value); } } + + class PongSignal extends DBusSignal { + public PongSignal(String _path, String _value) throws DBusException { + super(_path, _value); + } + } } } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/SignalNameTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/SignalNameTest.java index 62a786890..65cd838a9 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/SignalNameTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/SignalNameTest.java @@ -13,6 +13,10 @@ import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + public class SignalNameTest extends AbstractBaseTest { /** @@ -26,35 +30,40 @@ public class SignalNameTest extends AbstractBaseTest { * @throws Exception */ @Test - void testSignalNameAlias() { - assertDoesNotThrow(() -> { - String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); - BusAddress busAddress = TransportBuilder. - createWithDynamicSession(protocolType) - .configure().build().getBusAddress(); - - BusAddress listenBusAddress = BusAddress.of(busAddress).getListenerAddress(); - - try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenBusAddress)) { - daemon.startInBackgroundAndWait(MAX_WAIT); - logger.debug("Started embedded bus on address {}", listenBusAddress); - - // connect to started daemon process - logger.info("Connecting to embedded DBus {}", busAddress); - - try (DBusConnection connection = DBusConnectionBuilder.forAddress(busAddress).build()) { - connection.requestBusName("d.e.f.Service"); - connection.exportObject("/d/e/f/custom", new MyCustomImpl()); - - connection.addSigHandler(CustomService.CustomSignal.class, s -> logger.debug("Received signal: {}", s.data)); - - connection.sendMessage(new CustomService.CustomSignal("/a/b/c/custom", "hello world")); - // wait to deliver message - Thread.sleep(1000); - - } + void testSignalNameAlias() throws Exception { + String protocolType = TransportBuilder.getRegisteredBusTypes().getFirst(); + BusAddress busAddress = TransportBuilder. + createWithDynamicSession(protocolType) + .configure().build().getBusAddress(); + + BusAddress listenBusAddress = BusAddress.of(busAddress).getListenerAddress(); + + try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenBusAddress)) { + daemon.startInBackgroundAndWait(MAX_WAIT); + logger.debug("Started embedded bus on address {}", listenBusAddress); + + // connect to started daemon process + logger.info("Connecting to embedded DBus {}", busAddress); + + try (DBusConnection connection = DBusConnectionBuilder.forAddress(busAddress).build()) { + connection.requestBusName("d.e.f.Service"); + connection.exportObject("/d/e/f/custom", new MyCustomImpl()); + + AtomicReference received = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + connection.addSigHandler(CustomService.CustomSignal.class, s -> { + received.set(s.data); + latch.countDown(); + }); + + // creating the signal with the aliased (DBusMemberName/DBusInterfaceName) name must work ... + connection.sendMessage(new CustomService.CustomSignal("/a/b/c/custom", "hello world")); + + // ... and it must actually be delivered with the correct payload + assertTrue(latch.await(MAX_WAIT, TimeUnit.MILLISECONDS), "aliased signal should have been received"); + assertEquals("hello world", received.get(), "received signal data should match the sent data"); } - }); + } } @DBusInterfaceName("d.e.f.Custom") diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/signals/handler/GenericHandlerWithDecode.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/signals/handler/GenericHandlerWithDecode.java index 4054fad94..e639b48d3 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/signals/handler/GenericHandlerWithDecode.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/test/helper/signals/handler/GenericHandlerWithDecode.java @@ -39,6 +39,15 @@ public String getExpectedStringResult() { return expectedStringResult; } + /** + * Whether a signal has already been received (i.e. its parameters were decoded). + * + * @return true once a signal was handled + */ + public boolean hasReceived() { + return parameters != null; + } + public Throwable getAssertionError() { return assertionError; } diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java index 77320f965..42e7950ac 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/NonceTcpTransportTest.java @@ -5,6 +5,7 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.test.AbstractBaseTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; @@ -57,7 +58,8 @@ void testNonceTcpConnectFailsWithWrongNonce() throws Exception { // corrupt the nonce file so the client sends a nonce that does not match the server's Files.write(nonceFile, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}); - assertThrows(Exception.class, () -> { + // a rejected nonce surfaces as a wrapped DBusException from the connection build + assertThrows(DBusException.class, () -> { // use a short timeout to keep connection retries (and thus the test) brief try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress) .transportConfig().withTimeout(1000).back() diff --git a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java index f4e1c6439..a026f3f3a 100644 --- a/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java +++ b/dbus-java-tests/src/test/java/org/freedesktop/dbus/transport/tcp/TcpFamilyBindTest.java @@ -5,6 +5,7 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.connections.transports.TransportBuilder; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.test.AbstractBaseTest; import org.freedesktop.dbus.utils.Util; import org.junit.jupiter.api.Test; @@ -88,7 +89,8 @@ void testUnsupportedFamilyFails() throws Exception { try (EmbeddedDBusDaemon daemon = new EmbeddedDBusDaemon(listenAddress)) { daemon.startInBackgroundAndWait(MAX_WAIT); - assertThrows(Exception.class, () -> { + // the invalid family is rejected while resolving the address, surfaced as a wrapped DBusException + assertThrows(DBusException.class, () -> { try (DBusConnection conn = DBusConnectionBuilder.forAddress(connectAddress) .transportConfig().withTimeout(1000).back() .withShared(false).build()) { From d59dd42aeffa31a3eb6fa398d847f26d7c95ed35 Mon Sep 17 00:00:00 2001 From: David M Date: Mon, 27 Jul 2026 16:15:20 +0200 Subject: [PATCH 36/38] Updated README --- README.md | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 05721eb08..ba2a28941 100644 --- a/README.md +++ b/README.md @@ -97,21 +97,34 @@ The library will remain open source and MIT licensed and can still be used, fork - Fixed SASL authentication issue when running in server mode in combination with unix sockets ([#298](https://github.com/hypfvieh/dbus-java/issues/298)) - Fixed various issues with `InterfaceCodeGenerator` ([#302](https://github.com/hypfvieh/dbus-java/issues/302), [#303](https://github.com/hypfvieh/dbus-java/issues/303), [#304], (https://github.com/hypfvieh/dbus-java/issues/304), [#306](https://github.com/hypfvieh/dbus-java/issues/306) - Refactoring and overhaul of `InterfaceCodeGenerator` to improve code, reduce duplications and allow easier fixing/extending - - Hardened message parsing against malformed or malicious wire data: unknown header field codes are now ignored instead of throwing, oversized arrays are rejected before the length is truncated by an `int` cast, and the data offset is included when validating string/signature lengths (previously an `ArrayIndexOutOfBoundsException`/`StringIndexOutOfBoundsException` could tear down the connection) - - Enforce an actual recursion-depth limit when converting wire types to Java types in `Marshalling` (the previous check accidentally used the type count instead of the nesting depth) - - Remove the empty match-rule queue when `AddMatch` fails, so a later `addSigHandler` re-sends `AddMatch` instead of silently never subscribing - - Disconnect the connection on an unexpected `RuntimeException` in the incoming-message thread instead of spinning in a busy-loop / flooding the log (`DBusException` still logs and continues) - - Notify and clean up pending async callbacks (`CallbackHandler`) on disconnect instead of leaking them - - Added a read-timeout watchdog for the SASL handshake to prevent connections from hanging indefinitely (e.g. Slowloris-style stalls over TCP) - - Fixed `disconnect()` stalling for the whole receiving-service shutdown timeout when called from within a signal handler - - Use constant-time comparison for `DBUS_COOKIE_SHA1` hash verification - - Fixed possible `ArrayIndexOutOfBoundsException` when parsing malformed cookie lines during SASL auth - - Bounded the pending-error queue to avoid unbounded memory growth for unhandled errors - - Clean up sender/receiver executor services when the connection fails to establish - Added support for interactive authorization ([#PR313](https://github.com/hypfvieh/dbus-java/issues/313)), thanks to ([unfamiliarS](https://github.com/unfamiliarS) - - Added interfaces `org.freedesktop.DBus.Verbose` and `org.freedesktop.DBus.Debug.Stats` - - Added support for address fallback. DBus Specification allow specifying of multiple addresses when connecting. These addresses should be tried in the given order until one is available or all fail. Previously dbus-java only used the first address, ignoring all others. This behavior is now fixed and behaves like the specification defines it - - Improved tests + - **AI assisted improvements** (the following changes were developed with AI assistance): + - Added `Monitoring`/`BecomeMonitor` as a runtime mode: `DBusConnection.becomeMonitor(List, DBusMonitorHandler)` delivers raw copies of the bus traffic; the `EmbeddedDBusDaemon` now mirrors traffic to monitor connections (eavesdropping intentionally omitted as it is deprecated by the specification) + - Added automatic server-side `ObjectManager` handling: `GetManagedObjects` is answered from the exported sub-tree and `InterfacesAdded`/`InterfacesRemoved` are emitted automatically; opt out with `withManualObjectManager(true)` or export a ready-made one via `exportObjectManager(path)` + - Optionally emit `PropertiesChanged` automatically when a `@DBusBoundProperty` setter is invoked (`withAutoEmitPropertiesChanged(true)`, disabled by default, honours `EmitsChangedSignal`) + - Added the debug/diagnostic interfaces `org.freedesktop.DBus.Debug.Stats` and `org.freedesktop.DBus.Verbose`, offered only by the new opt-in `DebuggableEmbeddedDBusDaemon` (a default daemon behaves like a production reference daemon and reports `UnknownMethod`) + - Declared the `ActivatableServicesChanged` signal, added `ReloadConfig` to the `org.freedesktop.DBus` interface and exposed the bus properties `Features`/`Interfaces` on the embedded daemon + - Fixed several D-Bus match-rule issues: multiple `argN`/`argNpath` constraints are now combined with logical AND, `argNpath` parsing was corrected, the argument index range is validated (0–63) and `path_namespace` now also validates the message path + - Added support for address fallback: the D-Bus specification allows specifying multiple addresses when connecting, which should be tried in order until one is available or all fail. Previously dbus-java only used the first address and ignored the rest; now all are tried as the specification defines (`DBusConnectionBuilder.forAddresses(...)` and `;`-separated address lists) + - `BusAddress` now parses `;`-separated address lists (`parseAll`) and correctly decodes/encodes `%HH` escape sequences + - Added the `nonce-tcp` transport (16-byte nonce handshake); extended the transport SPI with a backward-compatible `ITransportProvider.getSupportedBusTypes()` so a single provider can serve several address schemes + - The TCP transport now honours the `family` (`ipv4`/`ipv6`) and `bind` address parameters (including `bind=*` to bind all interfaces) + - The unix transports now honour the listen-side address parameters `dir`, `tmpdir` and `runtime` (resolved to a concrete socket path/abstract name); introduced a shared `AbstractUnixBusAddress` base for the unix transports + - Fixed file-descriptor negotiation during SASL (`NEGOTIATE_UNIX_FD`/`AGREE_UNIX_FD`), including handling of servers that reject file-descriptor passing + - Hardened message parsing against malformed or malicious wire data: unknown header field codes are now ignored instead of throwing, oversized arrays are rejected before the length is truncated by an `int` cast, and the data offset is included when validating string/signature lengths (previously an `ArrayIndexOutOfBoundsException`/`StringIndexOutOfBoundsException` could tear down the connection) + - Enforce an actual recursion-depth limit when converting wire types to Java types in `Marshalling` (the previous check accidentally used the type count instead of the nesting depth) + - Validate the received `UNIX_FDS` count against the actual number of descriptors, bounds-check file-descriptor indices and guard against excessively nested variants/containers during value extraction + - Remove the empty match-rule queue when `AddMatch` fails, so a later `addSigHandler` re-sends `AddMatch` instead of silently never subscribing + - Disconnect the connection on an unexpected `RuntimeException` in the incoming-message thread instead of spinning in a busy-loop / flooding the log (`DBusException` still logs and continues) + - Notify and clean up pending async callbacks (`CallbackHandler`) on disconnect instead of leaking them + - Added a read-timeout watchdog for the SASL handshake to prevent connections from hanging indefinitely (e.g. Slowloris-style stalls over TCP) + - Fixed `disconnect()` stalling for the whole receiving-service shutdown timeout when called from within a signal handler + - Use constant-time comparison for `DBUS_COOKIE_SHA1` hash verification + - Fixed possible `ArrayIndexOutOfBoundsException` when parsing malformed cookie lines during SASL auth + - Bounded the pending-error queue to avoid unbounded memory growth for unhandled errors + - Clean up sender/receiver executor services when the connection fails to establish + - Reworked and corrected the project documentation under `src/site` (types, quickstart, properties, signals, code generation, new landing page) and documented intentionally unsupported transports (`unixexec`, `launchd`, `autolaunch`) + - Improved and hardened the test suite (event-driven waits instead of fixed sleeps, safer test teardown and additional coverage for the features above) ##### Changes in 5.2.0 (2025-12-21): - removed properties from dbus-java.version which causes issues with reproducable builds ([PR#279](https://github.com/hypfvieh/dbus-java/issues/279)) From f93fb241da9a0b2de036c67916a10b8a2da2a669 Mon Sep 17 00:00:00 2001 From: David M Date: Mon, 27 Jul 2026 17:01:18 +0200 Subject: [PATCH 37/38] Addressed sonar findings --- .../dbus/connections/BusAddress.java | 6 ++- .../freedesktop/dbus/connections/SASL.java | 2 +- .../base/ConnectionMessageHandler.java | 39 ++++++++++++------- .../impl/DBusConnectionBuilder.java | 3 ++ .../transports/AbstractUnixBusAddress.java | 21 +++++----- .../dbus/transport/tcp/TcpTransport.java | 7 +++- 6 files changed, 51 insertions(+), 27 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java index cc4e20c85..3b8b2a776 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/BusAddress.java @@ -186,13 +186,15 @@ private static String unescapeValue(String _value) { byte[] raw = _value.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream out = new ByteArrayOutputStream(raw.length); - for (int i = 0; i < raw.length; i++) { + int i = 0; + while (i < raw.length) { byte b = raw[i]; if (b == '%' && i + 2 < raw.length && isHex(raw[i + 1]) && isHex(raw[i + 2])) { out.write((Character.digit(raw[i + 1], 16) << 4) | Character.digit(raw[i + 2], 16)); - i += 2; + i += 3; } else { out.write(b); + i++; } } return out.toString(StandardCharsets.UTF_8); diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java index f27cb360a..5ec400c0f 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/SASL.java @@ -219,7 +219,7 @@ private void addCookie(String _context, String _id, long _timestamp, String _coo switch (classifyCookieLine(s, _timestamp)) { case KEEP -> lines.add(s); case MALFORMED -> logger.warn("Ignoring malformed cookie line {}", s); - case EXPIRED -> { } // silently drop stale cookie + case EXPIRED -> { /* silently drop stale (expired) cookie */ } } } } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java index 8ca7cca99..1c9df99c3 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/base/ConnectionMessageHandler.java @@ -491,19 +491,7 @@ protected Map>> collectManagedInterfaces(Exported Map> props = new LinkedHashMap<>(); // read bound properties declared on this interface - for (Entry pe : _eo.getPropertyMethods().entrySet()) { - Method getter = pe.getValue(); - if (pe.getKey().getAccess() == Access.READ && getter.getDeclaringClass() == iface) { - try { - Object value = getter.invoke(obj); - if (value != null) { - props.put(pe.getKey().getName(), toVariant(value, getter.getGenericReturnType())); - } - } catch (Exception _ex) { - getLogger().debug("Failed to read bound property {} for managed objects", pe.getKey().getName(), _ex); - } - } - } + collectBoundProperties(_eo, iface, obj, props); // objects implementing the Properties interface directly if (obj instanceof Properties p) { @@ -522,6 +510,31 @@ protected Map>> collectManagedInterfaces(Exported return byInterface; } + /** + * Reads the readable bound properties ({@code @DBusBoundProperty}) declared on the given interface via their + * getters and adds them to the property map. + * + * @param _eo exported object + * @param _iface interface whose bound properties should be read + * @param _obj the exported object instance + * @param _props target map to add the property values to + */ + private void collectBoundProperties(ExportedObject _eo, Class _iface, DBusInterface _obj, Map> _props) { + for (Entry pe : _eo.getPropertyMethods().entrySet()) { + Method getter = pe.getValue(); + if (pe.getKey().getAccess() == Access.READ && getter.getDeclaringClass() == _iface) { + try { + Object value = getter.invoke(_obj); + if (value != null) { + _props.put(pe.getKey().getName(), toVariant(value, getter.getGenericReturnType())); + } + } catch (Exception _ex) { + getLogger().debug("Failed to read bound property {} for managed objects", pe.getKey().getName(), _ex); + } + } + } + } + /** * Returns the object path of the closest exported {@link ObjectManager} which is an ancestor of the * given path, or {@code null} if none exists. diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java index 0813b99c2..b36d4205f 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/impl/DBusConnectionBuilder.java @@ -220,6 +220,9 @@ public DBusConnection build() throws DBusException { removedConnection.close(); } } + } else { + // close the freshly created (but not registered) connection to avoid leaking its threads/transport + c.close(); } throw _ex; } diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java index 220db2cbf..e80c8254d 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/connections/transports/AbstractUnixBusAddress.java @@ -26,6 +26,9 @@ public abstract class AbstractUnixBusAddress extends BusAddress implements IFile private static final SecureRandom RANDOM = new SecureRandom(); private static final int RANDOM_CHARS = 10; + private static final String PATH = "path"; + private static final String ABSTRACT = "abstract"; + /** * Creates a new unix bus address from the given (already parsed) address and resolves the listen-side * {@code dir}/{@code tmpdir}/{@code runtime} parameters. @@ -41,19 +44,19 @@ protected AbstractUnixBusAddress(BusAddress _obj, boolean _supportsAbstract) thr } public boolean hasPath() { - return hasParameter("path"); + return hasParameter(PATH); } public String getPath() { - return getParameterValue("path"); + return getParameterValue(PATH); } public boolean isAbstract() { - return hasParameter("abstract"); + return hasParameter(ABSTRACT); } public String getAbstract() { - return getParameterValue("abstract"); + return getParameterValue(ABSTRACT); } @Override @@ -66,7 +69,7 @@ public void updatePermissions(String _fileOwner, String _fileGroup, Set Date: Mon, 27 Jul 2026 17:21:13 +0200 Subject: [PATCH 38/38] More sonar findings: Simplified method --- .../org/freedesktop/dbus/Marshalling.java | 168 +++++++++--------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java index c0e55f58a..14c287316 100644 --- a/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java +++ b/dbus-java-core/src/main/java/org/freedesktop/dbus/Marshalling.java @@ -388,88 +388,18 @@ private static int getJavaType(String _dbusType, List _resultValue, int _l try { int idx = 0; for (; idx < _dbusType.length() && (-1 == _limit || _limit > _resultValue.size()); idx++) { - switch (_dbusType.charAt(idx)) { - case ArgumentType.STRUCT1: - int structIdx = idx + 1; - for (int structLen = 1; structLen > 0; structIdx++) { - if (ArgumentType.STRUCT2 == _dbusType.charAt(structIdx)) { - structLen--; - } else if (ArgumentType.STRUCT1 == _dbusType.charAt(structIdx)) { - structLen++; + char sigChar = _dbusType.charAt(idx); + switch (sigChar) { + case ArgumentType.STRUCT1 -> idx = parseStruct(_dbusType, idx, _resultValue, _depth); + case ArgumentType.ARRAY -> idx = parseArray(_dbusType, idx, _resultValue, _depth); + case ArgumentType.DICT_ENTRY1 -> idx = parseDictEntry(_dbusType, idx, _resultValue, _depth); + default -> { + Type simpleType = simpleJavaType(sigChar); + if (simpleType == null) { + throw new DBusException(String.format("Failed to parse DBus type signature: %s (%s).", _dbusType, sigChar)); } + _resultValue.add(simpleType); } - - List contained = new ArrayList<>(); - getJavaType(_dbusType.substring(idx + 1, structIdx - 1), contained, -1, _depth + 1); - _resultValue.add(new DBusStructType(contained.toArray(EMPTY_TYPE_ARRAY))); - idx = structIdx - 1; //-1 because j already points to the next signature char - break; - case ArgumentType.ARRAY: - if (ArgumentType.DICT_ENTRY1 == _dbusType.charAt(idx + 1)) { - contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 2), contained, 2, _depth + 1); - _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); - idx += javaType + 2; - } else { - contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 1, _depth + 1); - _resultValue.add(new DBusListType(contained.getFirst())); - idx += javaType; - } - break; - case ArgumentType.VARIANT: - _resultValue.add(Variant.class); - break; - case ArgumentType.BOOLEAN: - _resultValue.add(Boolean.class); - break; - case ArgumentType.INT16: - _resultValue.add(Short.class); - break; - case ArgumentType.BYTE: - _resultValue.add(Byte.class); - break; - case ArgumentType.OBJECT_PATH: - _resultValue.add(DBusPath.class); - break; - case ArgumentType.UINT16: - _resultValue.add(UInt16.class); - break; - case ArgumentType.INT32: - _resultValue.add(Integer.class); - break; - case ArgumentType.UINT32: - _resultValue.add(UInt32.class); - break; - case ArgumentType.INT64: - _resultValue.add(Long.class); - break; - case ArgumentType.UINT64: - _resultValue.add(UInt64.class); - break; - case ArgumentType.DOUBLE: - _resultValue.add(Double.class); - break; - case ArgumentType.FLOAT: - _resultValue.add(Float.class); - break; - case ArgumentType.STRING: - _resultValue.add(CharSequence.class); - break; - case ArgumentType.FILEDESCRIPTOR: - _resultValue.add(FileDescriptor.class); - break; - case ArgumentType.SIGNATURE: - _resultValue.add(Type[].class); - break; - case ArgumentType.DICT_ENTRY1: - contained = new ArrayList<>(); - int javaType = getJavaType(_dbusType.substring(idx + 1), contained, 2, _depth + 1); - _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); - idx += javaType + 1; - break; - default: - throw new DBusException(String.format("Failed to parse DBus type signature: %s (%s).", _dbusType, _dbusType.charAt(idx))); } } return idx; @@ -479,6 +409,84 @@ private static int getJavaType(String _dbusType, List _resultValue, int _l } } + /** + * Maps a scalar (non-container) DBus type signature character to its Java type. + * + * @param _sigChar signature character + * @return the Java type, or {@code null} if the character is not a scalar type + */ + private static Type simpleJavaType(char _sigChar) { + return switch (_sigChar) { + case ArgumentType.VARIANT -> Variant.class; + case ArgumentType.BOOLEAN -> Boolean.class; + case ArgumentType.INT16 -> Short.class; + case ArgumentType.BYTE -> Byte.class; + case ArgumentType.OBJECT_PATH -> DBusPath.class; + case ArgumentType.UINT16 -> UInt16.class; + case ArgumentType.INT32 -> Integer.class; + case ArgumentType.UINT32 -> UInt32.class; + case ArgumentType.INT64 -> Long.class; + case ArgumentType.UINT64 -> UInt64.class; + case ArgumentType.DOUBLE -> Double.class; + case ArgumentType.FLOAT -> Float.class; + case ArgumentType.STRING -> CharSequence.class; + case ArgumentType.FILEDESCRIPTOR -> FileDescriptor.class; + case ArgumentType.SIGNATURE -> Type[].class; + default -> null; + }; + } + + /** + * Parses a struct ({@code (...)}) starting at {@code _idx} and appends a {@link DBusStructType}. + * + * @return the index of the last consumed signature character + */ + private static int parseStruct(String _dbusType, int _idx, List _resultValue, int _depth) throws DBusException { + int structIdx = _idx + 1; + for (int structLen = 1; structLen > 0; structIdx++) { + if (ArgumentType.STRUCT2 == _dbusType.charAt(structIdx)) { + structLen--; + } else if (ArgumentType.STRUCT1 == _dbusType.charAt(structIdx)) { + structLen++; + } + } + + List contained = new ArrayList<>(); + getJavaType(_dbusType.substring(_idx + 1, structIdx - 1), contained, -1, _depth + 1); + _resultValue.add(new DBusStructType(contained.toArray(EMPTY_TYPE_ARRAY))); + return structIdx - 1; // -1 because structIdx already points to the next signature char + } + + /** + * Parses an array ({@code a...}) starting at {@code _idx} and appends a {@link DBusListType} or, for a dict entry + * element, a {@link DBusMapType}. + * + * @return the index of the last consumed signature character + */ + private static int parseArray(String _dbusType, int _idx, List _resultValue, int _depth) throws DBusException { + List contained = new ArrayList<>(); + if (ArgumentType.DICT_ENTRY1 == _dbusType.charAt(_idx + 1)) { + int javaType = getJavaType(_dbusType.substring(_idx + 2), contained, 2, _depth + 1); + _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); + return _idx + javaType + 2; + } + int javaType = getJavaType(_dbusType.substring(_idx + 1), contained, 1, _depth + 1); + _resultValue.add(new DBusListType(contained.getFirst())); + return _idx + javaType; + } + + /** + * Parses a dict entry ({@code {...}}) starting at {@code _idx} and appends a {@link DBusMapType}. + * + * @return the index of the last consumed signature character + */ + private static int parseDictEntry(String _dbusType, int _idx, List _resultValue, int _depth) throws DBusException { + List contained = new ArrayList<>(); + int javaType = getJavaType(_dbusType.substring(_idx + 1), contained, 2, _depth + 1); + _resultValue.add(new DBusMapType(contained.getFirst(), contained.get(1))); + return _idx + javaType + 1; + } + /** * Recursively converts types for serialization onto DBus.
    *