From fcc3c2ce4a69c124879e80f2ce844a952bdc6322 Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Sat, 27 Jan 2024 19:30:47 +0100 Subject: [PATCH] Eliminate netconf.nettyutil.handler.ssh.client All of this code has been rendered unused, remove it. JIRA: NETCONF-1108 Change-Id: I3543a42edd7f2464544af6a54036444ee923cf01 Signed-off-by: Robert Varga --- .../authentication/LoginPasswordHandler.java | 37 -- .../handler/ssh/client/AsyncSshHandler.java | 322 ----------- .../ssh/client/AsyncSshHandlerReader.java | 114 ---- .../ssh/client/AsyncSshHandlerWriter.java | 220 -------- .../client/AuthenticationFailedException.java | 30 - .../ssh/client/NetconfClientBuilder.java | 66 --- .../ssh/client/NetconfClientSessionImpl.java | 62 --- .../ssh/client/NetconfSessionFactory.java | 27 - .../handler/ssh/client/NetconfSshClient.java | 22 - .../ssh/client/NettyAwareClientSession.java | 43 -- .../ssh/client/NettyChannelSubsystem.java | 62 --- .../handler/ssh/client/package-info.java | 12 - .../LoginPasswordHandlerTest.java | 34 -- .../ssh/client/AsyncSshHandlerTest.java | 519 ------------------ .../ssh/client/AsyncSshHandlerWriterTest.java | 44 -- 15 files changed, 1614 deletions(-) delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandler.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AuthenticationFailedException.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientBuilder.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientSessionImpl.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSessionFactory.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSshClient.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyAwareClientSession.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyChannelSubsystem.java delete mode 100644 netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/package-info.java delete mode 100644 netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandlerTest.java delete mode 100644 netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java delete mode 100644 netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriterTest.java diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandler.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandler.java deleted file mode 100644 index 2245152f08..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.authentication; - -import java.io.IOException; -import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession; - -/** - * Class Providing username/password authentication option to - * {@link org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandler}. - */ -public final class LoginPasswordHandler extends AuthenticationHandler { - private final String username; - private final String password; - - public LoginPasswordHandler(final String username, final String password) { - this.username = username; - this.password = password; - } - - @Override - public String getUsername() { - return username; - } - - @Override - public AuthFuture authenticate(final ClientSession session) throws IOException { - session.addPasswordIdentity(password); - return session.auth(); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java deleted file mode 100644 index c9660544ef..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandler.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static com.google.common.base.Verify.verify; -import static java.util.Objects.requireNonNull; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.FutureListener; -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.net.SocketAddress; -import java.time.Duration; -import java.util.concurrent.TimeUnit; -import org.checkerframework.checker.lock.qual.GuardedBy; -import org.checkerframework.checker.lock.qual.Holding; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.netconf.api.TransportConstants; -import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler; -import org.opendaylight.netconf.shaded.sshd.client.channel.ChannelSubsystem; -import org.opendaylight.netconf.shaded.sshd.client.channel.ClientChannel; -import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture; -import org.opendaylight.netconf.shaded.sshd.client.future.ConnectFuture; -import org.opendaylight.netconf.shaded.sshd.client.future.OpenFuture; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession; -import org.opendaylight.netconf.shaded.sshd.core.CoreModuleProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Netty SSH handler class. Acts as interface between Netty and SSH library. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public final class AsyncSshHandler extends ChannelOutboundHandlerAdapter { - private static final Logger LOG = LoggerFactory.getLogger(AsyncSshHandler.class); - private static final VarHandle DISCONNECTED; - - static { - try { - DISCONNECTED = MethodHandles.lookup().findVarHandle(AsyncSshHandler.class, "disconnected", boolean.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new ExceptionInInitializerError(e); - } - } - - public static final int SSH_DEFAULT_NIO_WORKERS = 8; - - public static final NetconfSshClient DEFAULT_CLIENT; - - static { - final var c = new NetconfClientBuilder().build(); - // Disable default timeouts from mina sshd - final var zero = Duration.ofMillis(0); - CoreModuleProperties.AUTH_TIMEOUT.set(c, zero); - CoreModuleProperties.IDLE_TIMEOUT.set(c, zero); - CoreModuleProperties.NIO2_READ_TIMEOUT.set(c, zero); - CoreModuleProperties.TCP_NODELAY.set(c, true); - - // TODO make configurable, or somehow reuse netty threadpool - c.setNioWorkers(SSH_DEFAULT_NIO_WORKERS); - c.start(); - DEFAULT_CLIENT = c; - } - - private final AuthenticationHandler authenticationHandler; - private final Future negotiationFuture; - private final NetconfSshClient sshClient; - private final String name; - - // Initialized by connect() - @GuardedBy("this") - private ChannelPromise connectPromise; - - private AsyncSshHandlerWriter sshWriteAsyncHandler; - private ChannelSubsystem channel; - private ClientSession session; - private FutureListener negotiationFutureListener; - - private volatile boolean disconnected; - - private AsyncSshHandler(final AuthenticationHandler authenticationHandler, final NetconfSshClient sshClient, - final @Nullable Future negotiationFuture, final @Nullable String name) { - this.authenticationHandler = requireNonNull(authenticationHandler); - this.sshClient = requireNonNull(sshClient); - this.negotiationFuture = negotiationFuture; - this.name = name != null && !name.isBlank() ? name : "UNNAMED"; - } - - public AsyncSshHandler(final AuthenticationHandler authenticationHandler, final NetconfSshClient sshClient, - final @Nullable Future negotiationFuture) { - this(authenticationHandler, sshClient, negotiationFuture, null); - } - - /** - * Constructor of {@code AsyncSshHandler}. - * - * @param authenticationHandler authentication handler - * @param sshClient started SshClient - */ - public AsyncSshHandler(final AuthenticationHandler authenticationHandler, final NetconfSshClient sshClient) { - this(authenticationHandler, sshClient, null); - } - - public static AsyncSshHandler createForNetconfSubsystem(final AuthenticationHandler authenticationHandler) { - return new AsyncSshHandler(authenticationHandler, DEFAULT_CLIENT); - } - - /** - * Create AsyncSshHandler for netconf subsystem. Negotiation future has to be set to success after successful - * netconf negotiation. - * - * @param authenticationHandler authentication handler - * @param negotiationFuture negotiation future - * @return {@code AsyncSshHandler} - */ - public static AsyncSshHandler createForNetconfSubsystem(final AuthenticationHandler authenticationHandler, - final Future negotiationFuture, final @Nullable NetconfSshClient sshClient, - final @Nullable String name) { - return new AsyncSshHandler(authenticationHandler, sshClient != null ? sshClient : DEFAULT_CLIENT, - negotiationFuture, name); - } - - @Override - public synchronized void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) { - sshWriteAsyncHandler.write(ctx, msg, promise); - } - - @Override - public synchronized void connect(final ChannelHandlerContext ctx, final SocketAddress remoteAddress, - final SocketAddress localAddress, final ChannelPromise promise) throws IOException { - LOG.debug("{}: SSH session connecting on channel {}. promise: {}", name, ctx.channel(), promise); - connectPromise = requireNonNull(promise); - - if (negotiationFuture != null) { - negotiationFutureListener = future -> { - if (future.isSuccess()) { - promise.setSuccess(); - } - }; - //complete connection promise with netconf negotiation future - negotiationFuture.addListener(negotiationFutureListener); - } - - LOG.debug("{}: Starting SSH to {} on channel: {}", name, remoteAddress, ctx.channel()); - sshClient.connect(authenticationHandler.getUsername(), remoteAddress) - // FIXME: this is a blocking call, we should handle this with a concurrently-scheduled timeout. We do not - // have a Timer ready, so perhaps we should be using the event loop? - .verify(ctx.channel().config().getConnectTimeoutMillis(), TimeUnit.MILLISECONDS) - .addListener(future -> onConnectComplete(future, ctx)); - } - - private synchronized void onConnectComplete(final ConnectFuture connectFuture, final ChannelHandlerContext ctx) { - final var cause = connectFuture.getException(); - if (cause != null) { - onOpenFailure(ctx, cause); - return; - } - - final var clientSession = connectFuture.getSession(); - LOG.trace("{}: SSH session {} created on channel: {}", name, clientSession, ctx.channel()); - verify(clientSession instanceof NettyAwareClientSession, "Unexpected session %s", clientSession); - - final var localSession = (NettyAwareClientSession) clientSession; - session = localSession; - - final AuthFuture authFuture; - try { - authFuture = authenticationHandler.authenticate(localSession); - } catch (final IOException e) { - onOpenFailure(ctx, e); - return; - } - - authFuture.addListener(future -> onAuthComplete(future, localSession, ctx)); - } - - private synchronized void onAuthComplete(final AuthFuture authFuture, final NettyAwareClientSession clientSession, - final ChannelHandlerContext ctx) { - final var cause = authFuture.getException(); - if (cause != null) { - onOpenFailure(ctx, new AuthenticationFailedException("Authentication failed", cause)); - return; - } - if (disconnected) { - LOG.debug("{}: Skipping SSH subsystem allocation, channel: {}", name, ctx.channel()); - return; - } - - LOG.debug("{}: SSH session authenticated on channel: {}, server version: {}", name, ctx.channel(), - clientSession.getServerVersion()); - - final OpenFuture openFuture; - try { - channel = clientSession.createSubsystemChannel(TransportConstants.SSH_SUBSYSTEM, ctx); - channel.setStreaming(ClientChannel.Streaming.Async); - openFuture = channel.open(); - } catch (final IOException e) { - onOpenFailure(ctx, e); - return; - } - - openFuture.addListener(future -> ctx.executor().execute(() -> onOpenComplete(future, ctx))); - } - - // This callback has to run on the channel's executor because it runs fireChannelActive(), which needs to be - // delivered synchronously. If we were to execute on some other thread we would end up delaying the event, - // potentially creating havoc in the pipeline. - private synchronized void onOpenComplete(final OpenFuture openFuture, final ChannelHandlerContext ctx) { - final var cause = openFuture.getException(); - if (cause != null) { - onOpenFailure(ctx, cause); - return; - } - if (disconnected) { - LOG.trace("{}: Skipping activation, channel: {}", name, ctx.channel()); - return; - } - - LOG.trace("{}: SSH subsystem channel opened successfully on channel: {}", name, ctx.channel()); - if (negotiationFuture == null) { - connectPromise.setSuccess(); - } - - sshWriteAsyncHandler = new AsyncSshHandlerWriter(channel.getAsyncIn()); - ctx.fireChannelActive(); - channel.onClose(() -> disconnect(ctx, ctx.newPromise())); - } - - @Holding("this") - private void onOpenFailure(final ChannelHandlerContext ctx, final Throwable cause) { - LOG.warn("{}: Unable to setup SSH connection on channel: {}", name, ctx.channel(), cause); - - // If the promise is not yet done, we have failed with initial connect and set connectPromise to failure - if (!connectPromise.isDone()) { - connectPromise.setFailure(cause); - } - - disconnect(ctx, ctx.newPromise()); - } - - @Override - public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) { - disconnect(ctx, promise); - } - - @Override - public void disconnect(final ChannelHandlerContext ctx, final ChannelPromise promise) { - if (DISCONNECTED.compareAndSet(this, false, true)) { - ctx.executor().execute(() -> safelyDisconnect(ctx, promise)); - } - } - - // This method has the potential to interact with the channel pipeline, for example via fireChannelInactive(). These - // callbacks need to complete during execution of this method and therefore this method needs to be executing on - // the channel's executor. - @SuppressWarnings("checkstyle:IllegalCatch") - private synchronized void safelyDisconnect(final ChannelHandlerContext ctx, final ChannelPromise promise) { - LOG.trace("{}: Closing SSH session on channel: {} with connect promise in state: {}", name, ctx.channel(), - connectPromise); - - // If we have already succeeded and the session was dropped after, - // we need to fire inactive to notify reconnect logic - if (connectPromise.isSuccess()) { - ctx.fireChannelInactive(); - } - - if (sshWriteAsyncHandler != null) { - sshWriteAsyncHandler.close(); - } - - //If connection promise is not already set, it means negotiation failed - //we must set connection promise to failure - if (!connectPromise.isDone()) { - connectPromise.setFailure(new IllegalStateException("Negotiation failed")); - } - - //Remove listener from negotiation future, we don't want notifications - //from negotiation anymore - if (negotiationFuture != null) { - negotiationFuture.removeListener(negotiationFutureListener); - } - - if (session != null && !session.isClosed() && !session.isClosing()) { - session.close(false).addListener(future -> { - synchronized (this) { - if (!future.isClosed()) { - session.close(true); - } - session = null; - } - }); - } - - // Super disconnect is necessary in this case since we are using NioSocketChannel and it needs - // to cleanup its resources e.g. Socket that it tries to open in its constructor - // (https://bugs.opendaylight.org/show_bug.cgi?id=2430) - // TODO better solution would be to implement custom ChannelFactory + Channel - // that will use mina SSH lib internally: port this to custom channel implementation - try { - // Disconnect has to be closed after inactive channel event was fired, because it interferes with it - super.disconnect(ctx, ctx.newPromise()); - } catch (final Exception e) { - LOG.warn("{}: Unable to cleanup all resources for channel: {}. Ignoring.", name, ctx.channel(), e); - } - - if (channel != null) { - channel.close(false); - channel = null; - } - promise.setSuccess(); - LOG.debug("{}: SSH session closed on channel: {}", name, ctx.channel()); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java deleted file mode 100644 index b123e0b631..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerReader.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ - -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import org.opendaylight.netconf.shaded.sshd.common.future.SshFutureListener; -import org.opendaylight.netconf.shaded.sshd.common.io.IoInputStream; -import org.opendaylight.netconf.shaded.sshd.common.io.IoReadFuture; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.Buffer; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.ByteArrayBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Listener on async input stream from SSH session. - * This listeners schedules reads in a loop until the session is closed or read fails. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public final class AsyncSshHandlerReader implements SshFutureListener, AutoCloseable { - private static final Logger LOG = LoggerFactory.getLogger(AsyncSshHandlerReader.class); - private static final int BUFFER_SIZE = 2048; - - private final AutoCloseable connectionClosedCallback; - private final ReadMsgHandler readHandler; - - private final String channelId; - private IoInputStream asyncOut; - private Buffer buf; - private IoReadFuture currentReadFuture; - - public AsyncSshHandlerReader(final AutoCloseable connectionClosedCallback, final ReadMsgHandler readHandler, - final String channelId, final IoInputStream asyncOut) { - this.connectionClosedCallback = connectionClosedCallback; - this.readHandler = readHandler; - this.channelId = channelId; - this.asyncOut = asyncOut; - buf = new ByteArrayBuffer(BUFFER_SIZE); - asyncOut.read(buf).addListener(this); - } - - @Override - public void operationComplete(final IoReadFuture future) { - if (checkDisconnect(future)) { - invokeDisconnect(); - } - } - - private synchronized boolean checkDisconnect(final IoReadFuture future) { - if (future.getException() != null) { - //if asyncout is already set to null by close method, do nothing - if (asyncOut == null) { - return false; - } - - if (asyncOut.isClosed() || asyncOut.isClosing()) { - // Ssh dropped - LOG.debug("Ssh session dropped on channel: {}", channelId, future.getException()); - } else { - LOG.warn("Exception while reading from SSH remote on channel {}", channelId, future.getException()); - } - return true; - } else if (future.getRead() > 0) { - final ByteBuf msg = Unpooled.wrappedBuffer(buf.array(), 0, future.getRead()); - if (LOG.isTraceEnabled()) { - LOG.trace("Reading message on channel: {}, message: {}", - channelId, AsyncSshHandlerWriter.byteBufToString(msg)); - } - readHandler.onMessageRead(msg); - - // Schedule next read - buf = new ByteArrayBuffer(BUFFER_SIZE); - currentReadFuture = asyncOut.read(buf); - currentReadFuture.addListener(this); - } - return false; - } - - /** - * Closing of the {@link AsyncSshHandlerReader}. This method should never be called with any locks held since - * call to {@link AutoCloseable#close()} can be a source of ABBA deadlock. - */ - @SuppressWarnings("checkstyle:IllegalCatch") - private void invokeDisconnect() { - try { - connectionClosedCallback.close(); - } catch (final Exception e) { - // This should not happen - throw new IllegalStateException(e); - } - } - - @Override - public synchronized void close() { - // Remove self as listener on close to prevent reading from closed input - if (currentReadFuture != null) { - currentReadFuture.removeListener(this); - currentReadFuture = null; - } - - asyncOut = null; - } - - public interface ReadMsgHandler { - - void onMessageRead(ByteBuf msg); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java deleted file mode 100644 index 412e94d0ae..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriter.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static com.google.common.base.Preconditions.checkState; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Deque; -import java.util.LinkedList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.checkerframework.checker.lock.qual.GuardedBy; -import org.opendaylight.netconf.shaded.sshd.common.io.IoOutputStream; -import org.opendaylight.netconf.shaded.sshd.common.io.WritePendingException; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.Buffer; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.ByteArrayBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Async Ssh writer. Takes messages(byte arrays) and sends them encrypted to remote server. - * Also handles pending writes by caching requests until pending state is over. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public final class AsyncSshHandlerWriter implements AutoCloseable { - private static final Logger LOG = LoggerFactory.getLogger(AsyncSshHandlerWriter.class); - private static final Pattern NON_ASCII = Pattern.compile("([^\\x20-\\x7E\\x0D\\x0A])+"); - - // public static final int MAX_PENDING_WRITES = 1000; - // TODO implement Limiting mechanism for pending writes - // But there is a possible issue with limiting: - // 1. What to do when queue is full ? Immediate Fail for every request ? - // 2. At this level we might be dealing with Chunks of messages(not whole messages) - // and unexpected behavior might occur when we send/queue 1 chunk and fail the other chunks - - private final Object asyncInLock = new Object(); - private volatile IoOutputStream asyncIn; - - // Order has to be preserved for queued writes - private final Deque pending = new LinkedList<>(); - - @GuardedBy("asyncInLock") - private boolean isWriteExecuted = false; - - public AsyncSshHandlerWriter(final IoOutputStream asyncIn) { - this.asyncIn = asyncIn; - } - - public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) { - if (asyncIn == null) { - promise.setFailure(new IllegalStateException("Channel closed")); - return; - } - // synchronized block due to deadlock that happens on ssh window resize - // writes and pending writes would lock the underlyinch channel session - // window resize write would try to write the message on an already locked channelSession - // while the pending write was in progress from the write callback - synchronized (asyncInLock) { - // TODO check for isClosed, isClosing might be performed by mina SSH internally and is not required here - // If we are closed/closing, set immediate fail - if (asyncIn.isClosed() || asyncIn.isClosing()) { - promise.setFailure(new IllegalStateException("Channel closed")); - } else { - final ByteBuf byteBufMsg = (ByteBuf) msg; - if (isWriteExecuted) { - queueRequest(ctx, byteBufMsg, promise); - return; - } - - writeWithPendingDetection(ctx, promise, byteBufMsg, false); - } - } - } - - //sending message with pending - //if resending message not succesfull, then attribute wasPending is true - private void writeWithPendingDetection(final ChannelHandlerContext ctx, final ChannelPromise promise, - final ByteBuf byteBufMsg, final boolean wasPending) { - try { - - if (LOG.isTraceEnabled()) { - LOG.trace("Writing request on channel: {}, message: {}", ctx.channel(), byteBufToString(byteBufMsg)); - } - - isWriteExecuted = true; - - asyncIn.writeBuffer(toBuffer(byteBufMsg)).addListener(future -> { - // synchronized block due to deadlock that happens on ssh window resize - // writes and pending writes would lock the underlying channel session - // window resize write would try to write the message on an already locked channelSession, - // while the pending write was in progress from the write callback - synchronized (asyncInLock) { - final var cause = future.getException(); - if (LOG.isTraceEnabled()) { - LOG.trace("Ssh write request finished on channel: {} with ex: {}, message: {}", ctx.channel(), - cause, byteBufToString(byteBufMsg)); - } - - // Notify success or failure - if (cause != null) { - LOG.warn("Ssh write request failed on channel: {} for message: {}", ctx.channel(), - byteBufToString(byteBufMsg), cause); - promise.setFailure(cause); - } else { - promise.setSuccess(); - } - - //rescheduling message from queue after successfully sent - if (wasPending) { - byteBufMsg.resetReaderIndex(); - pending.remove(); - } - - // Not needed anymore, release - byteBufMsg.release(); - } - - // Check pending queue and schedule next - // At this time we are guaranteed that we are not in pending state anymore - // so the next request should succeed - writePendingIfAny(); - }); - - } catch (final IOException | WritePendingException e) { - if (!wasPending) { - queueRequest(ctx, byteBufMsg, promise); - } - } - } - - private void writePendingIfAny() { - synchronized (asyncInLock) { - final PendingWriteRequest pendingWrite = pending.peek(); - if (pendingWrite == null) { - isWriteExecuted = false; - return; - } - - final ByteBuf msg = pendingWrite.msg; - if (LOG.isTraceEnabled()) { - LOG.trace("Writing pending request on channel: {}, message: {}", - pendingWrite.ctx.channel(), byteBufToString(msg)); - } - - writeWithPendingDetection(pendingWrite.ctx, pendingWrite.promise, msg, true); - } - } - - public static String byteBufToString(final ByteBuf msg) { - final String message = msg.toString(StandardCharsets.UTF_8); - msg.resetReaderIndex(); - Matcher matcher = NON_ASCII.matcher(message); - return matcher.replaceAll(data -> { - StringBuilder buf = new StringBuilder(); - buf.append("\""); - for (byte b : data.group().getBytes(StandardCharsets.US_ASCII)) { - buf.append(String.format("%02X", b)); - } - buf.append("\""); - return buf.toString(); - }); - } - - private void queueRequest(final ChannelHandlerContext ctx, final ByteBuf msg, final ChannelPromise promise) { - LOG.debug("Write pending on channel: {}, queueing, current queue size: {}", ctx.channel(), pending.size()); - if (LOG.isTraceEnabled()) { - LOG.trace("Queueing request due to pending: {}", byteBufToString(msg)); - } - -// try { - final var req = new PendingWriteRequest(ctx, msg, promise); - // Preconditions.checkState(pending.size() < MAX_PENDING_WRITES, - // "Too much pending writes(%s) on channel: %s, remote window is not getting read or is too small", - // pending.size(), ctx.channel()); - checkState(pending.offer(req), "Cannot pend another request write (pending count: %s) on channel: %s", - pending.size(), ctx.channel()); -// } catch (final Exception ex) { -// LOG.warn("Unable to queue write request on channel: {}. Setting fail for the request: {}", -// ctx.channel(), ex, byteBufToString(msg)); -// msg.release(); -// promise.setFailure(ex); -// } - } - - @Override - public void close() { - asyncIn = null; - } - - private static Buffer toBuffer(final ByteBuf msg) { - // TODO Buffer vs ByteBuf translate, Can we handle that better ? - msg.resetReaderIndex(); - final byte[] temp = new byte[msg.readableBytes()]; - msg.readBytes(temp, 0, msg.readableBytes()); - return new ByteArrayBuffer(temp); - } - - private static final class PendingWriteRequest { - private final ChannelHandlerContext ctx; - private final ByteBuf msg; - private final ChannelPromise promise; - - PendingWriteRequest(final ChannelHandlerContext ctx, final ByteBuf msg, final ChannelPromise promise) { - this.ctx = ctx; - // Reset reader index, last write (failed) attempt moved index to the end - msg.resetReaderIndex(); - this.msg = msg; - this.promise = promise; - } - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AuthenticationFailedException.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AuthenticationFailedException.java deleted file mode 100644 index 557441e884..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AuthenticationFailedException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static java.util.Objects.requireNonNull; - -import com.google.common.annotations.Beta; -import org.opendaylight.netconf.shaded.sshd.common.SshException; - -/** - * Exception reported when endpoint authentication fails. - */ -@Beta -public class AuthenticationFailedException extends SshException { - @java.io.Serial - private static final long serialVersionUID = 1L; - - public AuthenticationFailedException(final String message) { - super(requireNonNull(message)); - } - - public AuthenticationFailedException(final String message, final Throwable cause) { - super(requireNonNull(message), cause); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientBuilder.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientBuilder.java deleted file mode 100644 index 1d8ba69a27..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientBuilder.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static com.google.common.base.Verify.verify; - -import com.google.common.collect.ImmutableList; -import java.util.List; -import java.util.stream.Stream; -import org.opendaylight.netconf.shaded.sshd.client.ClientBuilder; -import org.opendaylight.netconf.shaded.sshd.client.SshClient; -import org.opendaylight.netconf.shaded.sshd.common.NamedFactory; -import org.opendaylight.netconf.shaded.sshd.common.kex.BuiltinDHFactories; -import org.opendaylight.netconf.shaded.sshd.common.kex.KeyExchangeFactory; -import org.opendaylight.netconf.shaded.sshd.common.signature.BuiltinSignatures; -import org.opendaylight.netconf.shaded.sshd.common.signature.Signature; - -/** - * A {@link ClientBuilder} which builds {@link NetconfSshClient} instances. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public class NetconfClientBuilder extends ClientBuilder { - // RFC8332 rsa-sha2-256/rsa-sha2-512 are not a part of Mina's default set of signatures for clients as of 2.5.1. - // Add them to ensure interop with modern highly-secured devices. - private static final ImmutableList> FULL_SIGNATURE_PREFERENCE = - Stream.concat(DEFAULT_SIGNATURE_PREFERENCE.stream(), Stream.of( - BuiltinSignatures.rsaSHA512, BuiltinSignatures.rsaSHA256)) - .filter(BuiltinSignatures::isSupported) - .distinct() - .collect(ImmutableList.toImmutableList()); - - // The SHA1 algorithm is disabled by default in Mina SSHD since 2.6.0. - // More details available here: https://issues.apache.org/jira/browse/SSHD-1004 - // This block adds diffie-hellman-group14-sha1 back to the list of supported algorithms. - private static final ImmutableList FULL_DH_FACTORIES_LIST = - Stream.concat(DEFAULT_KEX_PREFERENCE.stream(), Stream.of(BuiltinDHFactories.dhg14)) - .collect(ImmutableList.toImmutableList()); - private static final List FULL_KEX_PREFERENCE = - NamedFactory.setUpTransformedFactories(true, FULL_DH_FACTORIES_LIST, DH2KEX); - - @Override - public NetconfSshClient build() { - final SshClient client = super.build(); - verify(client instanceof NetconfSshClient, "Unexpected client %s", client); - return (NetconfSshClient) client; - } - - @Override - protected ClientBuilder fillWithDefaultValues() { - if (factory == null) { - factory = NetconfSshClient::new; - } - if (signatureFactories == null) { - signatureFactories = FULL_SIGNATURE_PREFERENCE; - } - if (keyExchangeFactories == null) { - keyExchangeFactories = FULL_KEX_PREFERENCE; - } - return super.fillWithDefaultValues(); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientSessionImpl.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientSessionImpl.java deleted file mode 100644 index 26e555dc09..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfClientSessionImpl.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static java.util.Objects.requireNonNull; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import java.io.IOException; -import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager; -import org.opendaylight.netconf.shaded.sshd.client.channel.ChannelSubsystem; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSessionImpl; -import org.opendaylight.netconf.shaded.sshd.common.io.IoSession; - -/** - * A {@link ClientSessionImpl} which additionally allows creation of NETCONF subsystem channel, which is routed to - * a particular {@link ChannelHandlerContext}. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public final class NetconfClientSessionImpl extends ClientSessionImpl implements NettyAwareClientSession { - public NetconfClientSessionImpl(final ClientFactoryManager client, final IoSession ioSession) throws Exception { - super(client, ioSession); - } - - @Override - public ChannelSubsystem createSubsystemChannel(final String subsystem, final ChannelHandlerContext ctx) - throws IOException { - requireNonNull(ctx); - return registerSubsystem(new NettyChannelSubsystem(subsystem) { - @Override - ChannelHandlerContext context() { - return ctx; - } - }); - } - - @Override - public ChannelSubsystem createSubsystemChannel(final String subsystem, - final ChannelPipeline pipeline) throws IOException { - requireNonNull(pipeline); - return registerSubsystem(new NettyChannelSubsystem(subsystem) { - @Override - ChannelHandlerContext context() { - return pipeline.firstContext(); - } - }); - } - - private ChannelSubsystem registerSubsystem(final ChannelSubsystem subsystem) throws IOException { - final var service = getConnectionService(); - final var id = service.registerChannel(subsystem); - if (log.isDebugEnabled()) { - log.debug("createSubsystemChannel({})[{}] created id={}", this, subsystem.getSubsystem(), id); - } - return subsystem; - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSessionFactory.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSessionFactory.java deleted file mode 100644 index 22de35a539..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSessionFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager; -import org.opendaylight.netconf.shaded.sshd.client.session.SessionFactory; -import org.opendaylight.netconf.shaded.sshd.common.io.IoSession; - -/** - * A {@link SessionFactory} which creates {@link NetconfClientSessionImpl}s. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public class NetconfSessionFactory extends SessionFactory { - public NetconfSessionFactory(final ClientFactoryManager client) { - super(client); - } - - @Override - protected NetconfClientSessionImpl doCreateSession(final IoSession ioSession) throws Exception { - return new NetconfClientSessionImpl(getClient(), ioSession); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSshClient.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSshClient.java deleted file mode 100644 index 7efdb268ae..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NetconfSshClient.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import org.opendaylight.netconf.shaded.sshd.client.SshClient; - -/** - * An extension to {@link SshClient} which uses {@link NetconfSessionFactory} to create sessions (leading towards - * {@link NetconfClientSessionImpl}. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public class NetconfSshClient extends SshClient { - @Override - protected NetconfSessionFactory createSessionFactory() { - return new NetconfSessionFactory(this); - } -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyAwareClientSession.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyAwareClientSession.java deleted file mode 100644 index 904d974125..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyAwareClientSession.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import java.io.IOException; -import org.opendaylight.netconf.shaded.sshd.client.channel.ChannelSubsystem; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession; - -/** - * A {@link ClientSession} which additionally allows subsystem channels which are forwarded to a particular Netty - * channel context. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -public interface NettyAwareClientSession extends ClientSession { - /** - * Allocate a channel to the specified subsystem. Incoming data on the channel will be routed to the - * specified {@link ChannelHandlerContext}. - * - * @param subsystem The subsystem name - * @param ctx Context to which to route data to - * @return The created {@link ChannelSubsystem} - * @throws IOException If failed to create the requested channel - */ - ChannelSubsystem createSubsystemChannel(String subsystem, ChannelHandlerContext ctx) throws IOException; - - /** - * Allocate a channel to the specified subsystem. Incoming data on the channel will be routed to the - * specified {@link ChannelPipeline}. - * - * @param subsystem The subsystem name - * @param pipeline ChannelPipeline to which to route data to - * @return The created {@link ChannelSubsystem} - * @throws IOException If failed to create the requested channel - */ - ChannelSubsystem createSubsystemChannel(String subsystem, ChannelPipeline pipeline) throws IOException; -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyChannelSubsystem.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyChannelSubsystem.java deleted file mode 100644 index 116a9bc53c..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/NettyChannelSubsystem.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import java.io.IOException; -import org.opendaylight.netconf.shaded.sshd.client.channel.ChannelSubsystem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Abstract base class for {@link ChannelSubsystem}s backed by a Netty {@link ChannelHandlerContext}. - */ -@Deprecated(since = "7.0.0", forRemoval = true) -abstract class NettyChannelSubsystem extends ChannelSubsystem { - private static final Logger LOG = LoggerFactory.getLogger(NettyChannelSubsystem.class); - - NettyChannelSubsystem(final String subsystem) { - super(subsystem); - } - - @Override - public final void close() { - close(false); - } - - @Override - protected final void doWriteExtendedData(final byte[] data, final int off, final long len) throws IOException { - // If we're already closing, ignore incoming data - if (isClosing()) { - return; - } - - LOG.debug("Discarding {} bytes of extended data", len); - if (len > 0) { - getLocalWindow().release(len); - } - } - - @Override - protected final void doWriteData(final byte[] data, final int off, final long len) throws IOException { - // If we're already closing, ignore incoming data - if (isClosing()) { - return; - } - - // TODO: consider using context's allocator for heap buffer here - final int reqLen = (int) len; - context().fireChannelRead(Unpooled.copiedBuffer(data, off, reqLen)); - if (reqLen > 0) { - getLocalWindow().release(reqLen); - } - } - - abstract ChannelHandlerContext context(); -} diff --git a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/package-info.java b/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/package-info.java deleted file mode 100644 index eb2f955687..0000000000 --- a/netconf/netconf-netty-util/src/main/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -/** - * Utilities for integration between Apache SSHD and Netty. Contains the wiring logic to extend SshClient to allow - * efficient shuffling of data towards the Netty channel. - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; \ No newline at end of file diff --git a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandlerTest.java b/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandlerTest.java deleted file mode 100644 index b82434c0d8..0000000000 --- a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/authentication/LoginPasswordHandlerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.authentication; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import org.junit.Test; -import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession; - -public class LoginPasswordHandlerTest { - @Test - public void testLoginPassword() throws Exception { - final LoginPasswordHandler loginPasswordHandler = new LoginPasswordHandler("user", "pwd"); - assertEquals("user", loginPasswordHandler.getUsername()); - - final ClientSession session = mock(ClientSession.class); - doNothing().when(session).addPasswordIdentity("pwd"); - doReturn(mock(AuthFuture.class)).when(session).auth(); - loginPasswordHandler.authenticate(session); - - verify(session).addPasswordIdentity("pwd"); - verify(session).auth(); - } -} diff --git a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java b/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java deleted file mode 100644 index 0342f2d195..0000000000 --- a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerTest.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.SettableFuture; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelConfig; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.channel.DefaultChannelPromise; -import io.netty.util.concurrent.EventExecutor; -import java.io.IOException; -import java.net.SocketAddress; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler; -import org.opendaylight.netconf.shaded.sshd.client.channel.ChannelSubsystem; -import org.opendaylight.netconf.shaded.sshd.client.channel.ClientChannel; -import org.opendaylight.netconf.shaded.sshd.client.future.AuthFuture; -import org.opendaylight.netconf.shaded.sshd.client.future.ConnectFuture; -import org.opendaylight.netconf.shaded.sshd.client.future.OpenFuture; -import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession; -import org.opendaylight.netconf.shaded.sshd.common.future.CloseFuture; -import org.opendaylight.netconf.shaded.sshd.common.future.SshFuture; -import org.opendaylight.netconf.shaded.sshd.common.future.SshFutureListener; -import org.opendaylight.netconf.shaded.sshd.common.io.IoInputStream; -import org.opendaylight.netconf.shaded.sshd.common.io.IoOutputStream; -import org.opendaylight.netconf.shaded.sshd.common.io.IoReadFuture; -import org.opendaylight.netconf.shaded.sshd.common.io.IoWriteFuture; -import org.opendaylight.netconf.shaded.sshd.common.io.WritePendingException; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.Buffer; -import org.opendaylight.netconf.shaded.sshd.common.util.buffer.ByteArrayBuffer; - -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class AsyncSshHandlerTest { - - @Mock - private NetconfSshClient sshClient; - @Mock - private AuthenticationHandler authHandler; - @Mock - private ChannelHandlerContext ctx; - @Mock - private Channel channel; - @Mock - private SocketAddress remoteAddress; - @Mock - private SocketAddress localAddress; - @Mock - private ChannelConfig channelConfig; - @Mock - private EventExecutor executor; - - private AsyncSshHandler asyncSshHandler; - - private SshFutureListener sshConnectListener; - private SshFutureListener sshAuthListener; - private SshFutureListener sshChannelOpenListener; - private ChannelPromise promise; - - @Before - public void setUp() throws Exception { - stubAuth(); - stubSshClient(); - stubChannel(); - stubCtx(); - - promise = getMockedPromise(); - - asyncSshHandler = new AsyncSshHandler(authHandler, sshClient); - } - - @After - public void tearDown() throws Exception { - sshConnectListener = null; - sshAuthListener = null; - sshChannelOpenListener = null; - promise = null; - asyncSshHandler.close(ctx, getMockedPromise()); - } - - private void stubAuth() throws IOException { - doReturn("usr").when(authHandler).getUsername(); - - final AuthFuture authFuture = mock(AuthFuture.class); - Futures.addCallback(stubAddListener(authFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - sshAuthListener = result; - } - }, MoreExecutors.directExecutor()); - doReturn(authFuture).when(authHandler).authenticate(any(ClientSession.class)); - } - - @SuppressWarnings("unchecked") - private static > ListenableFuture> stubAddListener(final T future) { - final SettableFuture> listenerSettableFuture = SettableFuture.create(); - - doAnswer(invocation -> { - listenerSettableFuture.set((SshFutureListener) invocation.getArguments()[0]); - return null; - }).when(future).addListener(any(SshFutureListener.class)); - - return listenerSettableFuture; - } - - private void stubCtx() { - doReturn(channel).when(ctx).channel(); - doReturn(ctx).when(ctx).fireChannelActive(); - doReturn(ctx).when(ctx).fireChannelInactive(); - doReturn(mock(ChannelFuture.class)).when(ctx).disconnect(any(ChannelPromise.class)); - doReturn(getMockedPromise()).when(ctx).newPromise(); - doReturn(executor).when(ctx).executor(); - doAnswer(invocation -> { - invocation.getArgument(0, Runnable.class).run(); - return null; - }).when(executor).execute(any()); - } - - private void stubChannel() { - doReturn("channel").when(channel).toString(); - } - - private void stubSshClient() throws IOException { - final ConnectFuture connectFuture = mock(ConnectFuture.class); - Futures.addCallback(stubAddListener(connectFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - sshConnectListener = result; - } - }, MoreExecutors.directExecutor()); - doReturn(connectFuture).when(sshClient).connect("usr", remoteAddress); - doReturn(channelConfig).when(channel).config(); - doReturn(1).when(channelConfig).getConnectTimeoutMillis(); - doReturn(connectFuture).when(connectFuture).verify(1,TimeUnit.MILLISECONDS); - } - - @Test - public void testConnectSuccess() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - verify(subsystemChannel).setStreaming(ClientChannel.Streaming.Async); - - verify(promise).setSuccess(); - verify(ctx).fireChannelActive(); - asyncSshHandler.close(ctx, getMockedPromise()); - verify(ctx).fireChannelInactive(); - } - - @Test - public void testWrite() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - final ChannelPromise writePromise = getMockedPromise(); - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0, 1, 2, 3, 4, 5}), writePromise); - - verify(writePromise).setSuccess(); - } - - @Test - public void testWriteClosed() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - - final IoWriteFuture ioWriteFuture = asyncIn.writeBuffer(new ByteArrayBuffer()); - - Futures.addCallback(stubAddListener(ioWriteFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - doReturn(new IllegalStateException()).when(ioWriteFuture).getException(); - result.operationComplete(ioWriteFuture); - } - }, MoreExecutors.directExecutor()); - - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - final ChannelPromise writePromise = getMockedPromise(); - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0,1,2,3,4,5}), writePromise); - - verify(writePromise).setFailure(any(Throwable.class)); - } - - @Test - public void testWritePendingOne() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final IoWriteFuture ioWriteFuture = asyncIn.writeBuffer(new ByteArrayBuffer()); - - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - final ChannelPromise firstWritePromise = getMockedPromise(); - - // intercept listener for first write, - // so we can invoke successful write later thus simulate pending of the first write - final ListenableFuture> firstWriteListenerFuture = - stubAddListener(ioWriteFuture); - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0,1,2,3,4,5}), firstWritePromise); - final SshFutureListener firstWriteListener = firstWriteListenerFuture.get(); - // intercept second listener, - // this is the listener for pending write for the pending write to know when pending state ended - final ListenableFuture> pendingListener = stubAddListener(ioWriteFuture); - - final ChannelPromise secondWritePromise = getMockedPromise(); - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0, 1, 2, 3, 4, 5}), secondWritePromise); - - doReturn(ioWriteFuture).when(asyncIn).writeBuffer(any(Buffer.class)); - - verifyNoMoreInteractions(firstWritePromise, secondWritePromise); - - // make first write stop pending - firstWriteListener.operationComplete(ioWriteFuture); - - // notify listener for second write that pending has ended - pendingListener.get().operationComplete(ioWriteFuture); - - // verify both write promises successful - verify(firstWritePromise).setSuccess(); - verify(secondWritePromise).setSuccess(); - } - - @Ignore("Pending queue is not limited") - @Test - public void testWritePendingMax() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final IoWriteFuture ioWriteFuture = asyncIn.writeBuffer(new ByteArrayBuffer()); - - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - final ChannelPromise firstWritePromise = getMockedPromise(); - - // intercept listener for first write, - // so we can invoke successful write later thus simulate pending of the first write - final ListenableFuture> firstWriteListenerFuture = - stubAddListener(ioWriteFuture); - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0,1,2,3,4,5}), firstWritePromise); - - final ChannelPromise secondWritePromise = getMockedPromise(); - // now make write throw pending exception - doThrow(WritePendingException.class).when(asyncIn).writeBuffer(any(Buffer.class)); - for (int i = 0; i < 1001; i++) { - asyncSshHandler.write(ctx, Unpooled.copiedBuffer(new byte[]{0, 1, 2, 3, 4, 5}), secondWritePromise); - } - - verify(secondWritePromise, times(1)).setFailure(any(Throwable.class)); - } - - @Test - public void testDisconnect() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - sshAuthListener.operationComplete(getSuccessAuthFuture()); - sshChannelOpenListener.operationComplete(getSuccessOpenFuture()); - - final ChannelPromise disconnectPromise = getMockedPromise(); - asyncSshHandler.disconnect(ctx, disconnectPromise); - - verify(sshSession).close(anyBoolean()); - verify(disconnectPromise).setSuccess(); - //verify(ctx).fireChannelInactive(); - } - - private static OpenFuture getSuccessOpenFuture() { - final OpenFuture openFuture = mock(OpenFuture.class); - doReturn(null).when(openFuture).getException(); - return openFuture; - } - - private static AuthFuture getSuccessAuthFuture() { - final AuthFuture authFuture = mock(AuthFuture.class); - doReturn(null).when(authFuture).getException(); - return authFuture; - } - - private static ConnectFuture getSuccessConnectFuture(final ClientSession sshSession) { - final ConnectFuture connectFuture = mock(ConnectFuture.class); - doReturn(null).when(connectFuture).getException(); - - doReturn(sshSession).when(connectFuture).getSession(); - return connectFuture; - } - - private static NettyAwareClientSession getMockedSshSession(final ChannelSubsystem subsystemChannel) - throws IOException { - final NettyAwareClientSession sshSession = mock(NettyAwareClientSession.class); - - doReturn("serverVersion").when(sshSession).getServerVersion(); - doReturn(false).when(sshSession).isClosed(); - doReturn(false).when(sshSession).isClosing(); - final CloseFuture closeFuture = mock(CloseFuture.class); - Futures.addCallback(stubAddListener(closeFuture), new SuccessFutureListener<>() { - @Override - public void onSuccess(final SshFutureListener result) { - doReturn(true).when(closeFuture).isClosed(); - result.operationComplete(closeFuture); - } - }, MoreExecutors.directExecutor()); - doReturn(closeFuture).when(sshSession).close(false); - - doReturn(subsystemChannel).when(sshSession).createSubsystemChannel(eq("netconf"), - any(ChannelHandlerContext.class)); - - return sshSession; - } - - private ChannelSubsystem getMockedSubsystemChannel(final IoInputStream asyncOut, - final IoOutputStream asyncIn) throws IOException { - final ChannelSubsystem subsystemChannel = mock(ChannelSubsystem.class); - - doNothing().when(subsystemChannel).setStreaming(any(ClientChannel.Streaming.class)); - final OpenFuture openFuture = mock(OpenFuture.class); - - Futures.addCallback(stubAddListener(openFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - sshChannelOpenListener = result; - } - }, MoreExecutors.directExecutor()); - - doReturn(openFuture).when(subsystemChannel).open(); - doReturn(asyncIn).when(subsystemChannel).getAsyncIn(); - doNothing().when(subsystemChannel).onClose(any()); - doReturn(null).when(subsystemChannel).close(false); - return subsystemChannel; - } - - private static IoOutputStream getMockedIoOutputStream() throws IOException { - final IoOutputStream mock = mock(IoOutputStream.class); - final IoWriteFuture ioWriteFuture = mock(IoWriteFuture.class); - doReturn(null).when(ioWriteFuture).getException(); - - Futures.addCallback(stubAddListener(ioWriteFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - result.operationComplete(ioWriteFuture); - } - }, MoreExecutors.directExecutor()); - - doReturn(ioWriteFuture).when(mock).writeBuffer(any(Buffer.class)); - doReturn(false).when(mock).isClosed(); - doReturn(false).when(mock).isClosing(); - return mock; - } - - private static IoInputStream getMockedIoInputStream() { - final IoInputStream mock = mock(IoInputStream.class); - final IoReadFuture ioReadFuture = mock(IoReadFuture.class); - // Always success for read - Futures.addCallback(stubAddListener(ioReadFuture), new SuccessFutureListener() { - @Override - public void onSuccess(final SshFutureListener result) { - result.operationComplete(ioReadFuture); - } - }, MoreExecutors.directExecutor()); - return mock; - } - - @Test - public void testConnectFailOpenChannel() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final IoInputStream asyncOut = getMockedIoInputStream(); - final IoOutputStream asyncIn = getMockedIoOutputStream(); - final ChannelSubsystem subsystemChannel = getMockedSubsystemChannel(asyncOut, asyncIn); - final ClientSession sshSession = getMockedSshSession(subsystemChannel); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - - sshAuthListener.operationComplete(getSuccessAuthFuture()); - - verify(subsystemChannel).setStreaming(ClientChannel.Streaming.Async); - - sshChannelOpenListener.operationComplete(getFailedOpenFuture()); - verify(promise).setFailure(any(Throwable.class)); - } - - @Test - public void testConnectFailAuth() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final NettyAwareClientSession sshSession = mock(NettyAwareClientSession.class); - doReturn(true).when(sshSession).isClosed(); - final ConnectFuture connectFuture = getSuccessConnectFuture(sshSession); - - sshConnectListener.operationComplete(connectFuture); - - final AuthFuture authFuture = getFailedAuthFuture(); - - sshAuthListener.operationComplete(authFuture); - verify(promise).setFailure(any(Throwable.class)); - asyncSshHandler.close(ctx, getMockedPromise()); - verify(ctx, times(0)).fireChannelInactive(); - } - - private static AuthFuture getFailedAuthFuture() { - final AuthFuture authFuture = mock(AuthFuture.class); - doReturn(new IllegalStateException()).when(authFuture).getException(); - return authFuture; - } - - private static OpenFuture getFailedOpenFuture() { - final OpenFuture openFuture = mock(OpenFuture.class); - doReturn(new IllegalStateException()).when(openFuture).getException(); - return openFuture; - } - - @Test - public void testConnectFail() throws Exception { - asyncSshHandler.connect(ctx, remoteAddress, localAddress, promise); - - final ConnectFuture connectFuture = getFailedConnectFuture(); - sshConnectListener.operationComplete(connectFuture); - verify(promise).setFailure(any(Throwable.class)); - } - - private static ConnectFuture getFailedConnectFuture() { - final ConnectFuture connectFuture = mock(ConnectFuture.class); - doReturn(new IllegalStateException()).when(connectFuture).getException(); - return connectFuture; - } - - private ChannelPromise getMockedPromise() { - return spy(new DefaultChannelPromise(channel)); - } - - private abstract static class SuccessFutureListener> - implements FutureCallback> { - - @Override - public abstract void onSuccess(SshFutureListener result); - - @Override - public void onFailure(final Throwable throwable) { - throw new RuntimeException(throwable); - } - } -} diff --git a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriterTest.java b/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriterTest.java deleted file mode 100644 index 9644315c47..0000000000 --- a/netconf/netconf-netty-util/src/test/java/org/opendaylight/netconf/nettyutil/handler/ssh/client/AsyncSshHandlerWriterTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.netconf.nettyutil.handler.ssh.client; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import io.netty.buffer.ByteBuf; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class AsyncSshHandlerWriterTest { - - private ByteBuf byteBuf; - - @Before - public void setUp() throws Exception { - byteBuf = mock(ByteBuf.class, Mockito.CALLS_REAL_METHODS); - doReturn(byteBuf).when(byteBuf).resetReaderIndex(); - } - - @Test - public void testByteBufToString() { - String testText = "Lorem Ipsum 0123456780!@#$%^&*<>\\|/?[]()\n\r"; - doReturn(testText).when(byteBuf).toString(ArgumentMatchers.any()); - assertEquals(testText, AsyncSshHandlerWriter.byteBufToString(byteBuf)); - - testText = "Lorem Ipsum" + (char) 0x8 + " 0123456780" + (char) 0x11 + (char) 0x7F + "9 !@#$%^&*<>\\|/?[]()\n\r"; - doReturn(testText).when(byteBuf).toString(ArgumentMatchers.any()); - assertEquals("Lorem Ipsum\"08\" 0123456780\"117F\"9 !@#$%^&*<>\\|/?[]()\n\r", - AsyncSshHandlerWriter.byteBufToString(byteBuf)); - } -} \ No newline at end of file -- 2.36.6