18 files modified
2 files added
| | |
| | | <dependency> |
| | | <groupId>ch.qos.logback</groupId> |
| | | <artifactId>logback-core</artifactId> |
| | | <version>1.5.33</version> |
| | | <version>1.5.35</version> |
| | | <exclusions> |
| | | <exclusion> |
| | | <artifactId>slf4j-api</artifactId> |
| | |
| | | /** |
| | | * Decode an ACI bind rule string representation. |
| | | * @param input The string representation of the bind rule. |
| | | * @return A BindRule class representing the bind rule. |
| | | * @return A BindRule class representing the bind rule, or {@code null} if |
| | | * {@code input} is null or blank. This null is the recursion base case for |
| | | * an empty group; callers must reject it rather than dereference it. |
| | | * @throws AciException If the string is an invalid bind rule. |
| | | */ |
| | | public static BindRule decode (String input) throws AciException { |
| | |
| | | String bindruleStr = input.trim(); |
| | | if (bindruleStr.isEmpty()) |
| | | { |
| | | // A blank bind rule (e.g. "( )") must be rejected as a |
| | | // syntax error rather than throwing StringIndexOutOfBoundsException. |
| | | // A blank bind rule (e.g. "( )") decodes to null rather than |
| | | // throwing StringIndexOutOfBoundsException on charAt(0). This null is |
| | | // the recursion base case for an empty group; the caller rejects it as |
| | | // a syntax error (see the bindrule_1 == null check below). |
| | | return null; |
| | | } |
| | | char firstChar = bindruleStr.charAt(0); |
| | |
| | | throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input)); |
| | | } |
| | | /* |
| | | * An empty group such as "()" or "( )" decodes to a null bind rule. |
| | | * Such a group is not a valid bind rule on its own, and using it as the |
| | | * left operand of an "and"/"or" (e.g. "()or userdn=...") would build a |
| | | * complex rule with a null left side that is later dereferenced during |
| | | * evaluation. Reject both cases here, before the remaining-chars test, |
| | | * so the message names the full offending input rather than a fragment. |
| | | */ |
| | | if (bindrule_1 == null) { |
| | | throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input)); |
| | | } |
| | | /* |
| | | * If there are remaining chars => there MUST be an operand (AND / OR) |
| | | * otherwise there is a syntax error |
| | | */ |
| | |
| | | boolean negate=determineNegation(ruleExpr); |
| | | remainingBindrule=ruleExpr.toString(); |
| | | BindRule bindrule_2 = BindRule.decode(remainingBindrule); |
| | | /* |
| | | * The right operand of an "and"/"or" bind rule must exist. It is null |
| | | * when the boolean operator is not followed by a bind rule, e.g. the |
| | | * "or" in "userdn=\"ldap:///self\" or" has nothing to its right |
| | | * (issue #719). |
| | | */ |
| | | if (bindrule_2 == null) { |
| | | throw new AciException( |
| | | WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr)); |
| | | } |
| | | bindrule_2.setNegate(negate); |
| | | return new BindRule(bindrule, bindrule_2, operand); |
| | | } |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2015 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.protocol; |
| | | |
| | |
| | | "concerned server ids: " + idList; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return getClass().getSimpleName() + " csn: " + csn + ", " + errorsToString(); |
| | | } |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2015 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | |
| | | package org.opends.server.replication.server; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.opends.server.replication.common.AssuredMode; |
| | | import org.opends.server.replication.common.CSN; |
| | |
| | | protected Map<Integer,Boolean> expectedServersAckStatus = new HashMap<>(); |
| | | |
| | | /** |
| | | * Immutable snapshot of the ids of the servers we expect an ack from, taken |
| | | * at construction time. Used for lock-free membership checks: the key set of |
| | | * {@link #expectedServersAckStatus} never changes after construction, but |
| | | * that map has its values mutated under lock by {@code processReceivedAck()}, |
| | | * so it must not be read concurrently without synchronization. |
| | | */ |
| | | private final Set<Integer> expectedServerIds; |
| | | |
| | | /** |
| | | * Facility for monitoring: |
| | | * If the timeout occurs for the original update, we call createAck(true) |
| | | * in the timeout code for sending back an error ack to the original server. |
| | |
| | | { |
| | | expectedServersAckStatus.put(serverId, false); |
| | | } |
| | | this.expectedServerIds = |
| | | Collections.unmodifiableSet(new HashSet<>(expectedServers)); |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether the provided server is one of the servers an ack is |
| | | * expected from for the matching update message. Reads an immutable snapshot |
| | | * taken at construction time, so it is safe to call without holding the lock |
| | | * on this object. |
| | | * |
| | | * @param serverId The serverId of the server. |
| | | * @return true if an ack is expected from the provided server. |
| | | */ |
| | | public boolean isExpectedServer(int serverId) |
| | | { |
| | | return expectedServerIds.contains(serverId); |
| | | } |
| | | |
| | | /** |
| | |
| | | private final int maxQueueBytesSize; |
| | | /** Specifies whether the consumer is following the producer (is not late). */ |
| | | private boolean following; |
| | | /** |
| | | * Specifies whether the last update message returned by |
| | | * {@link #getNextMessage()} was re-read from the changelog DB (catch-up |
| | | * path) rather than taken from the in-memory {@link #msgQueue}. Only ever |
| | | * accessed by the single consumer thread calling {@link #getNextMessage()}. |
| | | */ |
| | | private boolean lastMessageFromLateQueue; |
| | | /** Specifies the current serverState of this handler. */ |
| | | private ServerState serverState; |
| | | /** Specifies the baseDN of the domain. */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether the last update message returned by |
| | | * {@link #getNextMessage()} was re-read from the changelog DB (catch-up |
| | | * path) rather than taken from the in-memory queue. |
| | | * <p> |
| | | * Must only be called from the consumer thread calling |
| | | * {@link #getNextMessage()}. |
| | | * |
| | | * @return true if the last returned update message came from the late queue |
| | | */ |
| | | protected boolean isLastMessageFromLateQueue() |
| | | { |
| | | return lastMessageFromLateQueue; |
| | | } |
| | | |
| | | /** |
| | | * Retrieves the name of this monitor provider. It should be unique among all |
| | | * monitor providers, including all instances of the same monitor provider. |
| | | * |
| | |
| | | msgQueue.consumeUpTo(msg); |
| | | if (updateServerState(msg)) |
| | | { |
| | | // the returned instance is the one re-read from the |
| | | // changelog DB, not its msgQueue equivalent |
| | | lastMessageFromLateQueue = true; |
| | | return msg; |
| | | } |
| | | } |
| | |
| | | } |
| | | if (updateServerState(msg)) |
| | | { |
| | | lastMessageFromLateQueue = true; |
| | | return msg; |
| | | } |
| | | continue; |
| | |
| | | * by the other server. |
| | | * Otherwise just loop to select the next message. |
| | | */ |
| | | lastMessageFromLateQueue = false; |
| | | return msg; |
| | | } |
| | | } |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.net.ConnectException; |
| | | import java.net.InetAddress; |
| | | import java.net.InetSocketAddress; |
| | | import java.net.ServerSocket; |
| | |
| | | } |
| | | int timeoutMS = MultimasterReplication.getConnectionTimeoutMS(); |
| | | socket.connect(remoteServerAddress.toInetSocketAddress(), timeoutMS); |
| | | if (isSelfConnection(socket)) |
| | | { |
| | | // While the remote RS is down, the kernel may pick its port as the |
| | | // local port of this connecting socket (TCP simultaneous open), |
| | | // "connecting" it to itself. Keeping such a socket open would hold |
| | | // the port and prevent the RS from binding it on restart. |
| | | throw new ConnectException("Connection to " + remoteServerAddress |
| | | + " is a TCP self-connect, no replication server is listening"); |
| | | } |
| | | session = replSessionSecurity.createClientSession(socket, timeoutMS); |
| | | |
| | | ReplicationServerHandler rsHandler = new ReplicationServerHandler( |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether the ack window for the provided CSN is still open and |
| | | * the provided server is one of the servers an ack is expected from. |
| | | * |
| | | * @param csn The CSN of the update message. |
| | | * @param serverId The serverId of the candidate acknowledging server. |
| | | * @return true if an ack from the provided server would be accounted for. |
| | | */ |
| | | boolean isExpectedAck(CSN csn, int serverId) |
| | | { |
| | | final ExpectedAcksInfo expectedAcksInfo = waitingAcks.get(csn); |
| | | return expectedAcksInfo != null && expectedAcksInfo.isExpectedServer(serverId); |
| | | } |
| | | |
| | | /** |
| | | * Process an ack received from a given server. |
| | | * |
| | | * @param ack The ack message received. |
| | |
| | | |
| | | this.generationId = generationId; |
| | | this.generationIdSavedStatus = false; |
| | | |
| | | // generationId gossip is purely event-driven: it only travels in the |
| | | // topology messages sent on connect/disconnect/status events. Re-advertise |
| | | // on every real transition so a peer that missed one converges on the next. |
| | | if (generationId > 0) |
| | | { |
| | | sendTopoInfoToAll(); |
| | | } |
| | | } |
| | | return oldGenerationId; |
| | | } |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2015 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | |
| | | // Get the ack status for the matching server |
| | | int ackingServerId = ackingServer.getServerId(); |
| | | boolean ackReceived = expectedServersAckStatus.get(ackingServerId); |
| | | Boolean ackReceived = expectedServersAckStatus.get(ackingServerId); |
| | | if (ackReceived == null) |
| | | { |
| | | // Ack from a server we were not expecting an ack from, for instance |
| | | // because the update was delivered to it with the assured flag of the |
| | | // original sender through the changelog catch-up path: ignore it. |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Received ack from not expected server id: " + |
| | | ackingServerId + " ack message: " + ackMsg); |
| | | } |
| | | return false; |
| | | } |
| | | if (ackReceived) |
| | | { |
| | | // Sanity check: this should never happen |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2015 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | { |
| | | // Get the ack status for the matching server |
| | | int ackingServerId = ackingServer.getServerId(); |
| | | boolean ackReceived = expectedServersAckStatus.get(ackingServerId); |
| | | Boolean ackReceived = expectedServersAckStatus.get(ackingServerId); |
| | | if (ackReceived == null) |
| | | { |
| | | // Ack from a server we were not expecting an ack from, for instance |
| | | // because the update was delivered to it with the assured flag of the |
| | | // original sender through the changelog catch-up path: ignore it. |
| | | if (logger.isTraceEnabled()) |
| | | { |
| | | logger.trace("Received ack from not expected server id: " |
| | | + ackingServerId + " ack message: " + ackMsg); |
| | | } |
| | | return false; |
| | | } |
| | | if (ackReceived) |
| | | { |
| | | // Sanity check: this should never happen |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | | import static org.opends.messages.ReplicationMessages.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.UnsupportedEncodingException; |
| | | import java.util.Random; |
| | | import java.util.concurrent.Semaphore; |
| | | import java.util.concurrent.TimeUnit; |
| | |
| | | */ |
| | | public UpdateMsg take() throws ChangelogException |
| | | { |
| | | final UpdateMsg msg = getNextMessage(); |
| | | UpdateMsg msg = getNextMessage(); |
| | | final boolean fromLateQueue = isLastMessageFromLateQueue(); |
| | | |
| | | acquirePermitInSendWindow(); |
| | | |
| | | if (msg != null) |
| | | { |
| | | // Updates re-read from the changelog DB (catch-up path) carry the |
| | | // assured flag, mode and safe data level of their original sender: |
| | | // the NotAssuredUpdateMsg substitution performed at publish time by |
| | | // ReplicationServerDomain.addUpdate() only exists on the in-memory |
| | | // queue path. Normalize them here: keep the assured flag only while |
| | | // the ack window is still open and this server is expected to ack. |
| | | // Messages taken from the in-memory queue already carry the |
| | | // publish-time decision and must NOT be revisited: the ack window may |
| | | // legitimately close (timeout, or enough acks already received) |
| | | // before a slow peer gets here - e.g. when acquirePermitInSendWindow() |
| | | // above blocks on a closed send window - and such a peer must still |
| | | // receive the assured flag it was deemed eligible for. Its late ack is |
| | | // then safely ignored by ReplicationServerDomain.processAck() and |
| | | // ExpectedAcksInfo.processReceivedAck(). |
| | | if (fromLateQueue && msg.isAssured() |
| | | && !replicationServerDomain.isExpectedAck(msg.getCSN(), serverId)) |
| | | { |
| | | msg = toNotAssuredUpdateMsg(msg); |
| | | } |
| | | incrementOutCount(); |
| | | if (msg.isAssured()) |
| | | { |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Substitutes a not assured version of the provided update message so that a |
| | | * peer not expected to acknowledge it does not receive it with the assured |
| | | * flag. |
| | | * <p> |
| | | * This is the counterpart, for the changelog catch-up path, of the |
| | | * {@link NotAssuredUpdateMsg} substitution performed on the in-memory queue |
| | | * path by ReplicationServerDomain.addUpdate(): updates re-read from the |
| | | * changelog DB keep the assured flag of their original sender and must be |
| | | * normalized in {@link #take()} before being handed to the ServerWriter. |
| | | */ |
| | | private UpdateMsg toNotAssuredUpdateMsg(UpdateMsg msg) |
| | | { |
| | | try |
| | | { |
| | | return new NotAssuredUpdateMsg(msg); |
| | | } |
| | | catch (UnsupportedEncodingException e) |
| | | { |
| | | // Could not build the not assured form (unexpected message encoding). |
| | | // Deliver the original message rather than dropping it: losing the |
| | | // update would break replication consistency, which is worse than a |
| | | // spurious ack - and such an ack is now safely ignored by |
| | | // ExpectedAcksInfo.processReceivedAck(). |
| | | logger.error(LocalizableMessage.raw( |
| | | "Could not substitute a not assured version of update message %s: %s", |
| | | msg, stackTraceToSingleLineString(e))); |
| | | return msg; |
| | | } |
| | | } |
| | | |
| | | private void acquirePermitInSendWindow() |
| | | { |
| | | boolean acquired = false; |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2023-2025 3A Systems LLC. |
| | | * Portions Copyright 2023-2026 3A Systems LLC. |
| | | * Portions Copyright 2025 Wren Security. |
| | | */ |
| | | package org.opends.server.replication.service; |
| | |
| | | } |
| | | int timeoutMS = MultimasterReplication.getConnectionTimeoutMS(); |
| | | socket.connect(HostPort.valueOf(serverURL).toInetSocketAddress(), timeoutMS); |
| | | if (isSelfConnection(socket)) |
| | | { |
| | | // While the RS is down, the kernel may pick the RS port as the local |
| | | // port of this reconnecting socket (TCP simultaneous open), |
| | | // "connecting" it to itself. Keeping such a socket open would hold |
| | | // the RS port and prevent the RS from binding it on restart. |
| | | throw new ConnectException("Connection to " + serverURL |
| | | + " is a TCP self-connect, no replication server is listening"); |
| | | } |
| | | newSession = replSessionSecurity.createClientSession(socket, timeoutMS); |
| | | boolean isSslEncryption = replSessionSecurity.isSslEncryption(); |
| | | |
| | |
| | | int serverRunningTheTask, Task initTask, int initWindow) |
| | | throws DirectoryException |
| | | { |
| | | final ImportExportContext ieCtx = acquireIEContext(false); |
| | | |
| | | /* |
| | | We manage the list of servers to initialize in order : |
| | | - to test at the end that all expected servers have reconnected |
| | |
| | | - to update the task with the server(s) where this test failed |
| | | */ |
| | | |
| | | Map<Integer, DSInfo> replicaInfos = getReplicaInfos(); |
| | | // Validate the request before acquiring the import/export context: a |
| | | // validation failure must not leave the context acquired, otherwise |
| | | // ieRunning() would remain true forever and the domain would reject every |
| | | // subsequent total update as a simultaneous import/export. |
| | | final Map<Integer, DSInfo> replicaInfos = getReplicaInfos(); |
| | | final DSInfo targetDsi; |
| | | if (serverToInitialize == RoutableMsg.ALL_SERVERS) |
| | | { |
| | | if (replicaInfos.isEmpty()) |
| | |
| | | throw new DirectoryException(UNWILLING_TO_PERFORM, |
| | | ERR_FULL_UPDATE_NO_REMOTES.get(getBaseDN(), getServerId())); |
| | | } |
| | | targetDsi = null; |
| | | } |
| | | else |
| | | { |
| | | targetDsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize); |
| | | if (targetDsi == null) |
| | | { |
| | | throw new DirectoryException(UNWILLING_TO_PERFORM, |
| | | ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize)); |
| | | } |
| | | } |
| | | |
| | | final ImportExportContext ieCtx = acquireIEContext(false); |
| | | try |
| | | { |
| | | initializeRemote(ieCtx, replicaInfos, targetDsi, serverToInitialize, |
| | | serverRunningTheTask, initTask, initWindow); |
| | | } |
| | | finally |
| | | { |
| | | // Release the context whatever the outcome, otherwise ieRunning() would |
| | | // remain true forever (resolves the historical "FIXME should not this |
| | | // be in a finally?"). |
| | | releaseIEContext(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Performs the remote initialization with the import/export context already |
| | | * acquired - and released - by the caller. |
| | | */ |
| | | private void initializeRemote(ImportExportContext ieCtx, |
| | | Map<Integer, DSInfo> replicaInfos, DSInfo targetDsi, |
| | | int serverToInitialize, int serverRunningTheTask, Task initTask, |
| | | int initWindow) throws DirectoryException |
| | | { |
| | | if (serverToInitialize == RoutableMsg.ALL_SERVERS) |
| | | { |
| | | logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START_ALL, |
| | | countEntries(), getBaseDN(), getServerId()); |
| | | |
| | |
| | | } |
| | | else |
| | | { |
| | | DSInfo dsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize); |
| | | if (dsi == null) |
| | | { |
| | | throw new DirectoryException(UNWILLING_TO_PERFORM, |
| | | ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize)); |
| | | } |
| | | |
| | | logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START, countEntries(), |
| | | getBaseDN(), getServerId(), serverToInitialize); |
| | | |
| | | ieCtx.startList.add(serverToInitialize); |
| | | ieCtx.setAckVal(dsi.getDsId(), 0); |
| | | ieCtx.setAckVal(targetDsi.getDsId(), 0); |
| | | } |
| | | |
| | | DirectoryException exportRootException = null; |
| | |
| | | ERR_INIT_NO_SUCCESS_END_FROM_SERVERS.get(getGenerationID(), ieCtx.failureList)); |
| | | } |
| | | |
| | | // Don't forget to release IEcontext acquired at beginning. |
| | | releaseIEContext(); // FIXME should not this be in a finally? |
| | | |
| | | final String cause = exportRootException == null ? "" |
| | | : exportRootException.getLocalizedMessage(); |
| | | if (serverToInitialize == RoutableMsg.ALL_SERVERS) |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | return true; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Indicates whether the provided connected socket is connected to itself, |
| | | * i.e. its local and remote endpoints are identical. |
| | | * <p> |
| | | * Connecting to a local port from the TCP ephemeral range while nothing |
| | | * listens on it can make the kernel pick that very port as the local port |
| | | * of the connecting socket: TCP simultaneous open then "establishes" the |
| | | * connection to itself (observed on Linux). Such a socket occupies the |
| | | * listen port its target service is about to bind, so callers retrying |
| | | * connections to a temporarily stopped service must detect and close it. |
| | | * |
| | | * @param socket a connected socket |
| | | * @return true if the socket is connected to itself |
| | | */ |
| | | public static boolean isSelfConnection(Socket socket) |
| | | { |
| | | return socket.getLocalPort() == socket.getPort() |
| | | && socket.getLocalAddress().equals(socket.getInetAddress()); |
| | | } |
| | | |
| | | /** |
| | | * Returns a lower-case string representation of a given string, verifying for null input string. |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.authorization.dseecompat; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.testng.annotations.AfterClass; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Verifies that {@link BindRule#decode(String)} rejects a boolean ("and"/"or") |
| | | * bind rule that is missing one of its operands, instead of throwing a |
| | | * {@link NullPointerException}. An empty group such as "()" decodes to a null |
| | | * bind rule; before this fix, using such a group as the left side, or leaving |
| | | * the right side of the boolean empty (e.g. "(()or)"), dereferenced that null |
| | | * while parsing and failed with an NPE rather than a diagnosable |
| | | * {@link AciException} (issue #719). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class BindRuleOperandTest extends DirectoryServerTestCase |
| | | { |
| | | @BeforeClass |
| | | public void setUp() throws Exception |
| | | { |
| | | TestCaseUtils.startFakeServer(); |
| | | } |
| | | |
| | | @AfterClass |
| | | public void tearDown() throws DirectoryException |
| | | { |
| | | TestCaseUtils.shutdownFakeServer(); |
| | | } |
| | | |
| | | @DataProvider(name = "bindRulesWithMissingOperand") |
| | | public Object[][] bindRulesWithMissingOperand() |
| | | { |
| | | return new Object[][] { |
| | | // the exact bind rule from issue #719: "or" with no right operand and an |
| | | // empty left group |
| | | { "(()or)" }, |
| | | // empty right operand after a valid left operand |
| | | { "(userdn=\"ldap:///self\" or)" }, |
| | | { "(userdn=\"ldap:///self\" and)" }, |
| | | // empty left operand before a valid right operand |
| | | { "()or userdn=\"ldap:///self\"" }, |
| | | // an empty group is not a bind rule on its own |
| | | { "()" }, |
| | | // a nested empty group is still empty |
| | | { "(())" }, |
| | | // a whitespace-only group is empty once trimmed |
| | | { "( )" }, |
| | | // empty right *group*: reaches the decode-level guard from createBindRule |
| | | { "(userdn=\"ldap:///self\" or ())" }, |
| | | // "not" applied to an empty group |
| | | { "not ()" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "bindRulesWithMissingOperand") |
| | | public void rejectsMissingOperand(String bindRule) |
| | | { |
| | | assertThatThrownBy(() -> BindRule.decode(bindRule)).isInstanceOf(AciException.class); |
| | | } |
| | | |
| | | @DataProvider(name = "validBindRules") |
| | | public Object[][] validBindRules() |
| | | { |
| | | return new Object[][] { |
| | | // a valid rule inside a nested group must still decode: guards must not |
| | | // start over-rejecting non-empty groups |
| | | { "((userdn=\"ldap:///self\"))" }, |
| | | // a valid complex rule with both operands present |
| | | { "(userdn=\"ldap:///self\" or userdn=\"ldap:///anyone\")" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "validBindRules") |
| | | public void acceptsValidBindRule(String bindRule) throws Exception |
| | | { |
| | | assertThat(BindRule.decode(bindRule)).isNotNull(); |
| | | } |
| | | |
| | | @DataProvider(name = "acisWithMissingOperand") |
| | | public Object[][] acisWithMissingOperand() |
| | | { |
| | | return new Object[][] { |
| | | // the full ACI from issue #719 |
| | | { "(version 3.0; acl \"ac\"; allow (search)(()or) (userdn=\"ldap:///self\"); )" }, |
| | | // missing *left* operand: previously accepted and stored with left == null, |
| | | // then NPEd during evaluation. Must now be rejected at decode time. |
| | | { "(version 3.0; acl \"ac\"; allow (search) ()or userdn=\"ldap:///self\"; )" }, |
| | | }; |
| | | } |
| | | |
| | | /** Reproduces missing-operand ACIs from issue #719 through {@link Aci#decode}. */ |
| | | @Test(dataProvider = "acisWithMissingOperand") |
| | | public void decodeOfAciWithMissingOperandThrowsAciException(String aci) |
| | | { |
| | | assertThatThrownBy(() -> Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN())) |
| | | .isInstanceOf(AciException.class); |
| | | } |
| | | } |
| | |
| | | import org.opends.server.types.Control; |
| | | import org.opends.server.types.Operation; |
| | | import org.opends.server.types.OperationType; |
| | | import org.opends.server.util.TestTimer; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import static java.util.concurrent.TimeUnit.*; |
| | | import static org.assertj.core.api.Assertions.*; |
| | | import static org.forgerock.opendj.ldap.ModificationType.*; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.*; |
| | |
| | | }; |
| | | assertEquals(LDAPDelete.run(nullPrintStream(), System.err, args), 0); |
| | | |
| | | assertNull(DirectoryServer.getAuthenticatedUsers().get(userDN)); |
| | | // AuthenticatedUsers is a POST_RESPONSE plugin: the registry is updated |
| | | // after the client already received the response, so poll briefly. |
| | | newAuthInfoTimer().repeatUntilSuccess(new TestTimer.CallableVoid() |
| | | { |
| | | @Override |
| | | public void call() throws Exception |
| | | { |
| | | assertNull(DirectoryServer.getAuthenticatedUsers().get(userDN)); |
| | | } |
| | | }); |
| | | } |
| | | finally |
| | | { |
| | |
| | | }; |
| | | assertEquals(LDAPModify.run(nullPrintStream(), System.err, args), 0); |
| | | |
| | | DN newUserDN = DN.valueOf("uid=test,ou=users,dc=example,dc=com"); |
| | | assertNotNull(DirectoryServer.getAuthenticatedUsers().get(newUserDN)); |
| | | assertEquals(DirectoryServer.getAuthenticatedUsers().get(newUserDN).size(), 1); |
| | | final DN newUserDN = DN.valueOf("uid=test,ou=users,dc=example,dc=com"); |
| | | // AuthenticatedUsers is a POST_RESPONSE plugin: the registry is updated |
| | | // after the client already received the response, so poll briefly. |
| | | newAuthInfoTimer().repeatUntilSuccess(new TestTimer.CallableVoid() |
| | | { |
| | | @Override |
| | | public void call() throws Exception |
| | | { |
| | | assertNotNull(DirectoryServer.getAuthenticatedUsers().get(newUserDN)); |
| | | assertEquals(DirectoryServer.getAuthenticatedUsers().get(newUserDN).size(), 1); |
| | | } |
| | | }); |
| | | } |
| | | finally |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Timer to await the asynchronous {@link AuthenticatedUsers} registry |
| | | * update: it is performed by a POST_RESPONSE plugin, i.e. after the client |
| | | * already received the operation response. |
| | | */ |
| | | private TestTimer newAuthInfoTimer() |
| | | { |
| | | return new TestTimer.Builder() |
| | | .maxSleep(10, SECONDS) |
| | | .sleepTimes(100, MILLISECONDS) |
| | | .toTimer(); |
| | | } |
| | | |
| | | /** |
| | | * Tests to ensure that the "ignore" password policy state update policy |
| | | * works as expected. |
| | | * |
| | |
| | | } |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | | waitForRemoteReplicas(server2ID); |
| | | addTask(taskInitTargetS2, ResultCode.SUCCESS, null); |
| | | |
| | | // Signal RS we just entered the full update status |
| | |
| | | } |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | | waitForRemoteReplicas(server2ID, server3ID); |
| | | addTask(taskInitTargetAll, ResultCode.SUCCESS, null); |
| | | |
| | | // Tests that entries have been received by S2 |
| | |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | | log(testCase + " add task " + Thread.currentThread()); |
| | | waitForRemoteReplicas(server2ID); |
| | | addTask(taskInitTargetS2, ResultCode.SUCCESS, null); |
| | | |
| | | log(testCase + " " + server2.getServerId() + " wait target " + Thread.currentThread()); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Tests that an InitializeTarget task failing its validation (the remote |
| | | * replica is unknown to the domain) does not leave the import/export |
| | | * context acquired (issue #730): the domain used to reject every subsequent |
| | | * total update as a simultaneous import/export. |
| | | */ |
| | | @Test(enabled=true) |
| | | public void initializeTargetUnknownRemote() throws Exception |
| | | { |
| | | String testCase = "initializeTargetUnknownRemote"; |
| | | log("Starting " + testCase); |
| | | try |
| | | { |
| | | replServer1 = createReplicationServer(replServer1ID, testCase); |
| | | connectServer1ToReplServer(replServer1ID); |
| | | addTestEntriesToDB(); |
| | | |
| | | // DS(42424) is unknown in the topology: the task must fail to start... |
| | | Entry taskInitTargetUnknown = TestCaseUtils.makeEntry( |
| | | "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", |
| | | "objectclass: top", |
| | | "objectclass: ds-task", |
| | | "objectclass: ds-task-initialize-remote-replica", |
| | | "ds-task-class-name: org.opends.server.tasks.InitializeTargetTask", |
| | | "ds-task-initialize-domain-dn: " + EXAMPLE_DN, |
| | | "ds-task-initialize-replica-server-id: " + 42424); |
| | | addTask(taskInitTargetUnknown, ResultCode.SUCCESS, null); |
| | | waitTaskState(taskInitTargetUnknown, STOPPED_BY_ERROR, 20000, null); |
| | | |
| | | // ...but must not leave the import/export context acquired |
| | | assertFalse(replDomain.ieRunning(), |
| | | "ReplicationDomain: Import/Export is not expected to be running after a failed InitializeTarget"); |
| | | |
| | | log("Successfully ending " + testCase); |
| | | } |
| | | finally |
| | | { |
| | | afterTest(testCase); |
| | | } |
| | | } |
| | | |
| | | private void waitForInitializeTargetMsg(String testCase, |
| | | ReplicationBroker server) throws Exception |
| | | { |
| | |
| | | * Disconnect broker and remove entries from the local DB |
| | | * @param testCase The name of the test case. |
| | | */ |
| | | /** |
| | | * Waits until the local replication domain sees the provided replicas in |
| | | * its topology view, so that an InitializeTarget task does not race the |
| | | * topology propagation and fail to start with "the remote directory server |
| | | * DS(x) is unknown" - which, combined with the import/export context leak |
| | | * (issue #730), used to poison the domain for the rest of the test class. |
| | | */ |
| | | private void waitForRemoteReplicas(Integer... serverIds) throws Exception |
| | | { |
| | | for (int serverId : serverIds) |
| | | { |
| | | for (int i = 0; i < 100 && !replDomain.getReplicaInfos().containsKey(serverId); i++) |
| | | { |
| | | sleep(100); |
| | | } |
| | | assertTrue(replDomain.getReplicaInfos().containsKey(serverId), |
| | | "DS(" + serverId + ") is not known to the local replication domain"); |
| | | } |
| | | } |
| | | |
| | | private void afterTest(String testCase) throws Exception |
| | | { |
| | | // Check that the domain has completed the import/export task. |
| | | boolean ieStillRunning = false; |
| | | if (replDomain != null) |
| | | { |
| | | // race condition could cause the main thread to reach |
| | |
| | | } |
| | | sleep(500); |
| | | } |
| | | assertFalse(replDomain.ieRunning(), "ReplicationDomain: Import/Export is not expected to be running"); |
| | | // asserted only after the cleanup below: failing before it would leak |
| | | // the domain config, the brokers and the replication servers into the |
| | | // following tests and cascade the failure over the whole class |
| | | ieStillRunning = replDomain.ieRunning(); |
| | | } |
| | | // Remove domain config |
| | | super.cleanConfigEntries(); |
| | |
| | | |
| | | Arrays.fill(replServerPort, 0); |
| | | log("Successfully cleaned " + testCase); |
| | | |
| | | assertFalse(ieStillRunning, "ReplicationDomain: Import/Export is not expected to be running"); |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | if (expectedTaskState == RUNNING && taskState == COMPLETED_SUCCESSFULLY) |
| | | if (expectedTaskState == RUNNING |
| | | && (taskState == COMPLETED_SUCCESSFULLY || taskState == STOPPED_BY_ERROR)) |
| | | { |
| | | // We usually wait the running state after adding the task |
| | | // and if the task is fast enough then it may be already done |
| | | // and we can go on. |
| | | // and if the task is fast enough then it may already be done |
| | | // (successfully or not) and we can go on: callers interested in the |
| | | // final state wait for it explicitly afterwards. |
| | | } |
| | | else |
| | | { |
| | |
| | | * |
| | | * Copyright 2008-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | * Portions Copyright 2023-2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.replication.ReplicationTestCase; |
| | | import org.opends.server.replication.common.AssuredMode; |
| | | import org.opends.server.replication.common.CSN; |
| | | import org.opends.server.replication.common.CSNGenerator; |
| | | import org.opends.server.replication.common.DSInfo; |
| | | import org.opends.server.replication.common.RSInfo; |
| | |
| | | * Sends a new update from this DS. |
| | | * @throws TimeoutException If timeout waiting for an assured ack |
| | | */ |
| | | private void sendNewFakeUpdate() throws TimeoutException |
| | | private CSN sendNewFakeUpdate() throws TimeoutException |
| | | { |
| | | // Create a new delete update message (the simplest to create) |
| | | DeleteMsg delMsg = new DeleteMsg(getBaseDN(), gen.newCSN(), UUID.randomUUID().toString()); |
| | |
| | | prepareWaitForAckIfAssuredEnabled(delMsg); |
| | | publish(delMsg); |
| | | waitForAckIfAssuredEnabled(delMsg); |
| | | return delMsg.getCSN(); |
| | | } |
| | | |
| | | private void assertReceivedWrongUpdates(int expectedNbUpdates, int expectedNbWrongUpdates) |
| | |
| | | private CSNGenerator gen; |
| | | |
| | | /** False if a received update had assured parameters not as expected. */ |
| | | private boolean everyUpdatesAreOk = true; |
| | | /** Number of received updates. */ |
| | | private int nReceivedUpdates; |
| | | private volatile boolean everyUpdatesAreOk = true; |
| | | /** |
| | | * Number of received updates. Volatile as it is incremented by the listen |
| | | * thread and polled from the test thread (waitForReceivedUpdates()); a read |
| | | * observing the incremented value also publishes everyUpdatesAreOk, which |
| | | * is written before the increment. |
| | | */ |
| | | private volatile int nReceivedUpdates; |
| | | |
| | | /** |
| | | * True if an ack has been replied to a received assured update (in assured |
| | |
| | | } |
| | | |
| | | /** |
| | | * Parameters for {@link #testCatchUpClearsAssuredFlag}: assured mode and |
| | | * safe data level of the update a peer catches up from the changelog. |
| | | */ |
| | | @DataProvider(name = "catchUpAssuredModeProvider") |
| | | private Object[][] catchUpAssuredModeProvider() |
| | | { |
| | | return new Object[][] |
| | | { |
| | | { AssuredMode.SAFE_DATA_MODE, 1 }, |
| | | { AssuredMode.SAFE_DATA_MODE, 2 }, |
| | | { AssuredMode.SAFE_READ_MODE, 1 }, |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Regression test for issue #710: an update served to a peer through the |
| | | * changelog catch-up path must not keep the assured flag of its original |
| | | * sender. |
| | | * <p> |
| | | * A fake RS connects with an empty server state after an assured update has |
| | | * already been received and persisted by the real RS. As the fake RS was not |
| | | * connected when the update was received, the update is re-read from the |
| | | * changelog DB (catch-up path) rather than served from the in-memory queue, |
| | | * and the fake RS is not an expected ack server for it. The update must |
| | | * therefore be forwarded with its assured flag cleared (while its assured |
| | | * mode and safe data level are preserved). This used to be delivered with |
| | | * assured=true, making the fake RS reply a spurious ack. Exercised across |
| | | * safe data (level 1 and level > 1) and safe read modes. |
| | | */ |
| | | @Test(dataProvider = "catchUpAssuredModeProvider", enabled = true) |
| | | public void testCatchUpClearsAssuredFlag(AssuredMode assuredMode, |
| | | int safeDataLevel) throws Exception |
| | | { |
| | | String testCase = "testCatchUpClearsAssuredFlag"; |
| | | debugInfo("Starting " + testCase + " mode=" + assuredMode + " sdl=" + safeDataLevel); |
| | | initTest(); |
| | | try |
| | | { |
| | | // Real RS to be tested |
| | | rs1 = createReplicationServer(RS1_ID, DEFAULT_GID, SMALL_TIMEOUT, testCase, 0); |
| | | |
| | | // Main DS sends an assured update, acknowledged by the RS. No other peer |
| | | // is connected yet, so the update only lands in the changelog DB, not in |
| | | // any peer message queue. |
| | | fakeRDs[1] = createFakeReplicationDomain(FDS1_ID, DEFAULT_GID, RS1_ID, |
| | | DEFAULT_GENID, assuredMode, safeDataLevel, LONG_TIMEOUT, TIMEOUT_DS_SCENARIO); |
| | | CSN csn = fakeRDs[1].sendNewFakeUpdate(); |
| | | |
| | | // Ensure the update is persisted before the peer connects, so the peer is |
| | | // served through the changelog catch-up path (the RS acks the DS before |
| | | // persisting, so returning from sendNewFakeUpdate() does not guarantee it). |
| | | waitForChangePersisted(rs1, csn); |
| | | |
| | | // A fake RS connects with an empty state: it must catch up the historical |
| | | // update from the changelog DB. It expects a non-assured forward and |
| | | // never replies an ack (TIMEOUT_RS_SCENARIO). |
| | | fakeRs1 = createFakeReplicationServer(FRS1_ID, DEFAULT_GID, DEFAULT_GENID, |
| | | false, assuredMode, safeDataLevel, TIMEOUT_RS_SCENARIO); |
| | | |
| | | // The historical update must have been received, with assured flag off |
| | | waitForReceivedUpdates(fakeRs1, 1); |
| | | fakeRs1.assertReceivedUpdates(1); |
| | | } |
| | | finally |
| | | { |
| | | endTest(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Waits until the provided change has been persisted in the changelog of the |
| | | * provided replication server, i.e. is retrievable through the catch-up path. |
| | | */ |
| | | private void waitForChangePersisted(ReplicationServer rs, CSN csn) throws Exception |
| | | { |
| | | ReplicationServerDomain domain = |
| | | rs.getReplicationServerDomain(DN.valueOf(TEST_ROOT_DN_STRING)); |
| | | assertNotNull(domain); |
| | | int i = 0; |
| | | while (!domain.getLatestServerState().cover(csn)) |
| | | { |
| | | if (i++ > 50) |
| | | { |
| | | Assert.fail("Change " + csn + " was not persisted in the changelog in time."); |
| | | } |
| | | Thread.sleep(100); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Waits until the provided fake RS has received at least the expected number |
| | | * of updates, so the assertion on the updates does not race with delivery. |
| | | */ |
| | | private void waitForReceivedUpdates(FakeReplicationServer fakeRs, int expected) |
| | | throws Exception |
| | | { |
| | | int i = 0; |
| | | while (fakeRs.nReceivedUpdates < expected && i++ <= 50) |
| | | { |
| | | Thread.sleep(100); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns possible combinations of parameters for testSafeDataLevelOne test. |
| | | */ |
| | | @DataProvider(name = "testSafeDataLevelOneProvider") |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | | import static org.mockito.Mockito.*; |
| | | import static org.testng.Assert.*; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.replication.common.CSN; |
| | | import org.opends.server.replication.protocol.AckMsg; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Test the handling of acks received from servers that were not expected to |
| | | * acknowledge an assured update. |
| | | * <p> |
| | | * A peer served through the changelog catch-up path receives the update with |
| | | * the assured flag of the original sender (see issue #710), so it may send |
| | | * back an ack for a server id that is not part of the expected servers of the |
| | | * matching {@link ExpectedAcksInfo}. That ack must be ignored instead of |
| | | * raising a {@link NullPointerException} while auto-unboxing the missing map |
| | | * entry, which would close the connection to the acking peer. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ExpectedAcksInfoTest extends DirectoryServerTestCase |
| | | { |
| | | private static final CSN CSN1 = new CSN(1, 1, 10); |
| | | |
| | | private static ServerHandler mockServerHandler(int serverId, boolean isDataServer) |
| | | { |
| | | ServerHandler handler = mock(ServerHandler.class); |
| | | when(handler.getServerId()).thenReturn(serverId); |
| | | when(handler.isDataServer()).thenReturn(isDataServer); |
| | | return handler; |
| | | } |
| | | |
| | | @Test |
| | | public void safeReadAckFromNotExpectedServerIsIgnored() |
| | | { |
| | | ServerHandler requester = mockServerHandler(1, true); |
| | | List<Integer> expectedServers = Arrays.asList(2, 3); |
| | | SafeReadExpectedAcksInfo acksInfo = new SafeReadExpectedAcksInfo( |
| | | CSN1, requester, expectedServers, Collections.<Integer> emptyList()); |
| | | |
| | | // Server id 99 is not in the expected servers list |
| | | ServerHandler notExpected = mockServerHandler(99, false); |
| | | assertFalse(acksInfo.processReceivedAck(notExpected, new AckMsg(CSN1)), |
| | | "An ack from a not expected server must not complete the ack info"); |
| | | assertFalse(acksInfo.isExpectedServer(99)); |
| | | assertTrue(acksInfo.isExpectedServer(2)); |
| | | } |
| | | |
| | | @Test |
| | | public void safeDataAckFromNotExpectedServerIsIgnored() |
| | | { |
| | | ServerHandler requester = mockServerHandler(1, true); |
| | | List<Integer> expectedServers = Arrays.asList(2, 3); |
| | | SafeDataExpectedAcksInfo acksInfo = new SafeDataExpectedAcksInfo( |
| | | CSN1, requester, (byte) 3, expectedServers); |
| | | |
| | | // Server id 99 is a RS not in the expected servers list |
| | | ServerHandler notExpected = mockServerHandler(99, false); |
| | | assertFalse(acksInfo.processReceivedAck(notExpected, new AckMsg(CSN1)), |
| | | "An ack from a not expected server must not complete the ack info"); |
| | | assertFalse(acksInfo.isExpectedServer(99)); |
| | | assertTrue(acksInfo.isExpectedServer(3)); |
| | | } |
| | | } |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.util; |
| | | |
| | |
| | | import java.io.IOException; |
| | | import java.io.PrintWriter; |
| | | import java.io.RandomAccessFile; |
| | | import java.net.InetSocketAddress; |
| | | import java.net.ServerSocket; |
| | | import java.net.Socket; |
| | | import java.nio.ByteBuffer; |
| | | import java.nio.channels.FileChannel; |
| | | import java.nio.channels.FileLock; |
| | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.testng.Assert; |
| | | import org.testng.SkipException; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | |
| | | new RuntimeException(new IllegalThreadStateException()), IllegalArgumentException.class); |
| | | Assert.assertTrue(hasCause, "Third case : IllegalThreadStateException should be detected as a cause"); |
| | | } |
| | | |
| | | @Test |
| | | public void testIsSelfConnectionFalseForNormalConnection() throws Exception |
| | | { |
| | | try (ServerSocket listener = new ServerSocket()) |
| | | { |
| | | listener.bind(new InetSocketAddress("127.0.0.1", 0)); |
| | | try (Socket client = new Socket()) |
| | | { |
| | | client.connect(listener.getLocalSocketAddress(), 2000); |
| | | try (Socket accepted = listener.accept()) |
| | | { |
| | | Assert.assertFalse(StaticUtils.isSelfConnection(client)); |
| | | Assert.assertFalse(StaticUtils.isSelfConnection(accepted)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | public void testIsSelfConnectionTrueForSelfConnectedSocket() throws Exception |
| | | { |
| | | // find a free port |
| | | final int port; |
| | | try (ServerSocket tmp = new ServerSocket()) |
| | | { |
| | | tmp.bind(new InetSocketAddress("127.0.0.1", 0)); |
| | | port = tmp.getLocalPort(); |
| | | } |
| | | |
| | | // Force the TCP self-connect (simultaneous open) that the kernel can |
| | | // produce spontaneously when connecting to an unbound port from the |
| | | // ephemeral range: bind the local end to the very port being connected. |
| | | try (Socket socket = new Socket()) |
| | | { |
| | | socket.setReuseAddress(true); |
| | | socket.bind(new InetSocketAddress("127.0.0.1", port)); |
| | | try |
| | | { |
| | | socket.connect(new InetSocketAddress("127.0.0.1", port), 2000); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | // BSD-based stacks (macOS) refuse an explicit self-connect |
| | | throw new SkipException("TCP self-connect not supported by this OS: " + e); |
| | | } |
| | | Assert.assertTrue(StaticUtils.isSelfConnection(socket)); |
| | | } |
| | | } |
| | | } |