This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit cbd61985ef49d17d6d1904fd7f96d42561129c7b Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Jan 8 09:33:53 2021 +0200 [SSHD-1097] Implemented endless tarpit example in sshd-contrib --- CHANGES.md | 1 + README.md | 3 + docs/event-listeners.md | 1 + docs/howto.md | 25 ++ .../org/apache/sshd/common/util/GenericUtils.java | 10 +- .../org/apache/sshd/common/util/buffer/Buffer.java | 27 +- .../sshd/common/util/buffer/ByteArrayBuffer.java | 18 +- .../sshd/contrib/common/io/EndlessWriteFuture.java | 90 +++++++ .../contrib/common/io/ImmediateWriteFuture.java | 32 +-- .../EndlessTarpitSenderSupportDevelopment.java | 280 +++++++++++++++++++++ .../sshd/client/session/AbstractClientSession.java | 13 +- .../sshd/client/session/ClientSessionImpl.java | 2 +- .../common/kex/extension/KexExtensionHandler.java | 88 +++---- .../session/ReservedSessionMessagesHandler.java | 21 +- .../sshd/common/session/SessionWorkBuffer.java | 3 +- .../common/session/helpers/AbstractSession.java | 77 +++--- .../sshd/common/session/helpers/SessionHelper.java | 22 +- .../sshd/server/session/AbstractServerSession.java | 2 +- .../sshd/server/session/ServerSessionImpl.java | 3 +- .../kex/extension/KexExtensionHandlerTest.java | 2 +- .../sshd/util/test/CoreTestSupportUtils.java | 10 +- .../sshd/scp/client/SimpleScpClientImpl.java | 3 +- 22 files changed, 589 insertions(+), 144 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5dada51..b2bc036 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ * [SSHD-1091](https://issues.apache.org/jira/browse/SSHD-1091) Renamed `sshd-contrib` top-level package in order to align naming convention. * [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Added more `SessionListener` callbacks related to the initial version and key exchange * [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Added more capability to send peer identification via `ReservedSessionMessagesHandler` +* [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Implemented [endless tarpit](https://nullprogram.com/blog/2019/03/22/) example in sshd-contrib * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress diff --git a/README.md b/README.md index ab548c6..9fab4f7 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ based applications requiring SSH support. * `copy-file`, `copy-data` - [DRAFT 00 - sections 6, 7](http://tools.ietf.org/id/draft-ietf-secsh-filexfer-extensions-00.txt) * `space-available` - [DRAFT 09 - section 9.3](http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt) * Several [OpenSSH SFTP extensions](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL) +* [Endless tarpit](https://nullprogram.com/blog/2019/03/22/) - see [HOWTO(s)](./docs/howto.md) section. ## Implemented/available support @@ -172,3 +173,5 @@ implementation of the logging API can be selected from the many existing adaptor ## [Configuration/data files parsing support](./docs/files-parsing.md) ## [Extension modules](./docs/extensions.md) + +# [HOWTO(s)](./docs/howto.md) \ No newline at end of file diff --git a/docs/event-listeners.md b/docs/event-listeners.md index 1f17a98..3527964 100644 --- a/docs/event-listeners.md +++ b/docs/event-listeners.md @@ -100,6 +100,7 @@ An **experimental** implementation example is available for the client side - se Can be used to handle the following cases: +* Intervene during the initial identification and KEX * [SSH_MSG_IGNORE](https://tools.ietf.org/html/rfc4253#section-11.2) * [SSH_MSG_DEBUG](https://tools.ietf.org/html/rfc4253#section-11.3) * [SSH_MSG_UNIMPLEMENTED](https://tools.ietf.org/html/rfc4253#section-11.4) diff --git a/docs/howto.md b/docs/howto.md new file mode 100644 index 0000000..e9ea60b --- /dev/null +++ b/docs/howto.md @@ -0,0 +1,25 @@ +# HOWTO(s) + +This section contains some useful "cookbook recipes" for getting the most out of the code. Please note that it does **not** covers code samples that already appear in the previous sections - such as creating sessions, managing authentication, 3-way SCP, mounting file systems via SFTP, etc... Instead, it focuses on more "exotic" implementations that are not usually part of the normal SSH flow. + +## [Endless tarpit](https://nullprogram.com/blog/2019/03/22/) + +In order to achieve this one needs to use a `ReservedSessionMessagesHandler` on the server side that overrides the session identification and KEX message callbacks as follows: + +* When `sendIdentification` callback is invoked + + * Check if you wish to trap the peer into the endless tarpit - if not, then return `null` + + * Spawn a thread that will feed the peer session with periodic infinite data. + + * Return a never succeeding `IoWriteFuture` - see `EndlessWriteFuture` in *sshd-contrib* package for such an implementation + +* When `sendKexInitRequest` callback is invoked + + * Check if you wish to trap the peer into the endless tarpit - if not, then return `null` + + * Return an `IoWriteFuture` that "succeeds" immediately - see `ImmediateWriteFuture` in *sshd-contrib* package for such an implementation. + +The idea is to prevent the normal session establish flow by taking over the initial handshake identification and blocking the initial KEX message from the server. + +A sample implementation can be found in the `EndlessTarpitSenderSupportDevelopment` class in the *sshd-contrib* package *test* section. \ No newline at end of file diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java index 9b53eb6..181a902 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -928,11 +928,15 @@ public final class GenericUtils { return current; } - public static IOException toIOException(Throwable e) { + public static void rethrowAsIoException(Throwable e) throws IOException { if (e instanceof IOException) { - return (IOException) e; + throw (IOException) e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else if (e instanceof Error) { + throw (Error) e; } else { - return new IOException(e); + throw new IOException(e); } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java index a691a8f..edf5137 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java @@ -157,18 +157,20 @@ public abstract class Buffer implements Readable { /** * Reset read/write positions to zero - <B>Note:</B> zeroes any previously existing data * - * @see #clear(boolean) + * @return Reference to this buffer + * @see #clear(boolean) */ - public void clear() { - clear(true); + public Buffer clear() { + return clear(true); } /** * Reset read/write positions to zero * - * @param wipeData Whether to zero any previously existing data + * @param wipeData Whether to zero any previously existing data + * @return Reference to this buffer */ - public abstract void clear(boolean wipeData); + public abstract Buffer clear(boolean wipeData); public boolean isValidMessageStructure(Class<?>... fieldTypes) { return isValidMessageStructure(GenericUtils.isEmpty(fieldTypes) ? Collections.emptyList() : Arrays.asList(fieldTypes)); @@ -992,17 +994,18 @@ public abstract class Buffer implements Readable { } } - protected void ensureCapacity(int capacity) { - ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR); + public Buffer ensureCapacity(int capacity) { + return ensureCapacity(capacity, BufferUtils.DEFAULT_BUFFER_GROWTH_FACTOR); } /** - * @param capacity The required capacity - * @param growthFactor An {@link IntUnaryOperator} that is invoked if the current capacity is insufficient. The - * argument is the minimum required new data length, the function result should be the effective - * new data length to be allocated - if less than minimum then an exception is thrown + * @param capacity The required capacity + * @param growthFactor An {@link IntUnaryOperator} that is invoked if the current capacity is insufficient. The + * argument is the minimum required new data length, the function result should be the + * effective new data length to be allocated - if less than minimum then an exception is thrown + * @return This buffer instance */ - public abstract void ensureCapacity(int capacity, IntUnaryOperator growthFactor); + public abstract Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor); /** * @return Current size of underlying backing data bytes array diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java index 5de36a6..04085ed 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java @@ -184,13 +184,15 @@ public class ByteArrayBuffer extends Buffer { } @Override - public void clear(boolean wipeData) { + public Buffer clear(boolean wipeData) { rpos = 0; wpos = 0; if (wipeData) { Arrays.fill(data, (byte) 0); } + + return this; } @Override @@ -207,11 +209,11 @@ public class ByteArrayBuffer extends Buffer { @Override public int putBuffer(Readable buffer, boolean expand) { - int r = expand ? buffer.available() : Math.min(buffer.available(), capacity()); - ensureCapacity(r); - buffer.getRawBytes(data, wpos, r); - wpos += r; - return r; + int required = expand ? buffer.available() : Math.min(buffer.available(), capacity()); + ensureCapacity(required); + buffer.getRawBytes(data, wpos, required); + wpos += required; + return required; } @Override @@ -259,7 +261,7 @@ public class ByteArrayBuffer extends Buffer { } @Override - public void ensureCapacity(int capacity, IntUnaryOperator growthFactor) { + public Buffer ensureCapacity(int capacity, IntUnaryOperator growthFactor) { ValidateUtils.checkTrue(capacity >= 0, "Negative capacity requested: %d", capacity); int maxSize = size(); @@ -276,6 +278,8 @@ public class ByteArrayBuffer extends Buffer { System.arraycopy(data, 0, tmp, 0, data.length); data = tmp; } + + return this; } @Override diff --git a/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java new file mode 100644 index 0000000..612c93b --- /dev/null +++ b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/EndlessWriteFuture.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.contrib.common.io; + +import java.io.IOException; + +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoWriteFuture; + +/** + * Never signals a successful write completion and ignores all listeners + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class EndlessWriteFuture implements IoWriteFuture { + public static final EndlessWriteFuture INSTANCE = new EndlessWriteFuture(); + + public EndlessWriteFuture() { + super(); + } + + @Override + public IoWriteFuture verify(long timeoutMillis) throws IOException { + await(timeoutMillis); + return null; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public Object getId() { + return "ENDLESS"; + } + + @Override + public boolean awaitUninterruptibly(long timeoutMillis) { + try { + Thread.sleep(timeoutMillis); + } catch (InterruptedException e) { + // ignored + } + + return false; + } + + @Override + public boolean await(long timeoutMillis) throws IOException { + return awaitUninterruptibly(timeoutMillis); + } + + @Override + public IoWriteFuture removeListener(SshFutureListener<IoWriteFuture> listener) { + return this; + } + + @Override + public IoWriteFuture addListener(SshFutureListener<IoWriteFuture> listener) { + return this; + } + + @Override + public boolean isWritten() { + return false; + } + + @Override + public Throwable getException() { + return null; + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java similarity index 55% copy from sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java copy to sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java index 00815a2..0711e40 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java +++ b/sshd-contrib/src/main/java/org/apache/sshd/contrib/common/io/ImmediateWriteFuture.java @@ -17,33 +17,19 @@ * under the License. */ -package org.apache.sshd.common.session; +package org.apache.sshd.contrib.common.io; -import java.util.Objects; - -import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.channel.IoWriteFutureImpl; +import org.apache.sshd.common.util.buffer.Buffer; /** + * Succeeds immediately upon construction + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ -public class SessionWorkBuffer extends ByteArrayBuffer implements SessionHolder<Session> { - private final Session session; - - public SessionWorkBuffer(Session session) { - this.session = Objects.requireNonNull(session, "No session"); - } - - @Override - public Session getSession() { - return session; - } - - @Override - public void clear(boolean wipeData) { - throw new UnsupportedOperationException("Not allowed to clear session work buffer of " + getSession()); - } - - public void forceClear(boolean wipeData) { - super.clear(wipeData); +public class ImmediateWriteFuture extends IoWriteFutureImpl { + public ImmediateWriteFuture(Object id, Buffer buffer) { + super(id, buffer); + setValue(Boolean.TRUE); } } diff --git a/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java new file mode 100644 index 0000000..9298b3b --- /dev/null +++ b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/EndlessTarpitSenderSupportDevelopment.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.contrib.server.session; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Arrays; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.Factory; +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.io.IoWriteFuture; +import org.apache.sshd.common.kex.KexProposalOption; +import org.apache.sshd.common.random.Random; +import org.apache.sshd.common.session.ReservedSessionMessagesHandler; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.session.SessionListener; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.io.NoCloseInputStream; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.contrib.common.io.EndlessWriteFuture; +import org.apache.sshd.contrib.common.io.ImmediateWriteFuture; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.CoreTestSupportUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @see <A HREF="https://nullprogram.com/blog/2019/03/22/">Endless tarpit</A> + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public final class EndlessTarpitSenderSupportDevelopment extends AbstractLoggingBean implements Runnable, SessionListener { + private static final Collection<EndlessTarpitSenderSupportDevelopment> THREADS = new LinkedList<>(); + + private final Random randomizer; + private final byte[] dataBuffer; + private final byte[] outputBuffer; + private AtomicLong numSent = new AtomicLong(); + private final ServerSession session; + private final AtomicBoolean okToRun = new AtomicBoolean(true); + + private EndlessTarpitSenderSupportDevelopment(ServerSession session, int lineLength) { + this.session = session; + this.dataBuffer = new byte[(lineLength * 4) / 6 /* BASE64 */]; + this.outputBuffer = new byte[lineLength + 8 /* some padding */ + 2 /* CRLF */]; + FactoryManager manager = session.getFactoryManager(); + Factory<Random> randomFactory = manager.getRandomFactory(); + this.randomizer = randomFactory.create(); + this.session.addSessionListener(this); + } + + @Override + public void sessionException(Session session, Throwable t) { + terminate("sessionException"); + } + + @Override + public void sessionDisconnect( + Session session, int reason, String msg, String language, boolean initiator) { + terminate("sessionDisconnect"); + } + + @Override + public void sessionClosed(Session session) { + terminate("sessionClosed"); + } + + private IoWriteFuture sendRandomLine() throws IOException { + randomizer.fill(dataBuffer); + + Encoder encoder = Base64.getEncoder(); + int len = encoder.encode(dataBuffer, outputBuffer); + outputBuffer[len] = (byte) '\r'; + outputBuffer[len + 1] = (byte) '\n'; + + byte[] packet = Arrays.copyOf(outputBuffer, len + 2); + String line = new String(packet, 0, packet.length - 2, StandardCharsets.US_ASCII); + IoSession networkSession = session.getIoSession(); + IoWriteFuture future = networkSession.writeBuffer(new ByteArrayBuffer(packet)); + long count = numSent.incrementAndGet(); + log.info("sendRandomLine({}) sent line #{}: {}", session, count, line); + return future; + } + + @Override + public void run() { + try { + synchronized (THREADS) { + THREADS.add(this); + } + + while (okToRun.get()) { + sendRandomLine(); + + synchronized (okToRun) { + okToRun.wait(TimeUnit.SECONDS.toMillis(5L)); + } + } + } catch (Exception e) { + log.error("run(" + session + ") failure", e); + } finally { + log.info("closing({})", session); + try { + session.close(true); + } finally { + session.removeSessionListener(this); + + synchronized (THREADS) { + THREADS.remove(this); + } + } + } + } + + private void terminate(Object logHint) { + boolean terminated; + synchronized (okToRun) { + terminated = okToRun.getAndSet(false); + okToRun.notifyAll(); + } + + if (terminated) { + log.info("terminate({}) terminated {}", logHint, session); + } + } + + ////////////////////////////////////////////////////////////////////////////////// + + private static <F extends FactoryManager> F setupTimeouts(F manager) { + CoreModuleProperties.NIO2_READ_TIMEOUT.set(manager, Duration.ofMinutes(15L)); + CoreModuleProperties.IDLE_TIMEOUT.set(manager, Duration.ZERO); + CoreModuleProperties.AUTH_TIMEOUT.set(manager, Duration.ZERO); + return manager; + } + + private static void startServer(String address, int port) throws Exception { + try (SshServer server = CoreTestSupportUtils.setupTestServer(EndlessTarpitSenderSupportDevelopment.class); + BufferedReader stdin = new BufferedReader( + new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) { + setupTimeouts(server); + + if (GenericUtils.isNotEmpty(address)) { + server.setHost(address); + } + server.setPort(port); + server.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { + private final Logger log = LoggerFactory.getLogger(EndlessTarpitSenderSupportDevelopment.class); + + @Override + @SuppressWarnings("synthetic-access") + public IoWriteFuture sendIdentification(Session session, String version, List<String> extraLines) + throws Exception { + EndlessTarpitSenderSupportDevelopment tarpit = new EndlessTarpitSenderSupportDevelopment( + (ServerSession) session, 32); + Thread thread = new Thread(tarpit, "t" + session.getIoSession().getRemoteAddress()); + thread.start(); + log.info("sendIdentification({})[{}] Started endless sender", session, version); + return EndlessWriteFuture.INSTANCE; + } + + @Override + public IoWriteFuture sendKexInitRequest( + Session session, Map<KexProposalOption, String> proposal, Buffer packet) + throws Exception { + log.info("sendKexInitRequest({}) suppressed KEX sending", session); + return new ImmediateWriteFuture(session, packet); + } + + }); + System.err.append("Starting SSHD on " + address + ":" + port); + server.start(); + + try { + while (true) { + System.out.println("Running on port " + port + " (Q)uit: "); + String line = stdin.readLine(); + line = GenericUtils.trimToEmpty(line); + if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line)) { + break; + } + } + } finally { + System.err.append("Stopping server on port ").println(port); + server.stop(); + } + } finally { + for (EndlessTarpitSenderSupportDevelopment t : THREADS) { + t.terminate("main"); + } + } + } + + private static void startClient(String host, int port) throws Exception { + try (SshClient client = CoreTestSupportUtils.setupTestClient(EndlessTarpitSenderSupportDevelopment.class)) { + setupTimeouts(client); + + client.addSessionListener(new SessionListener() { + private final Logger log = LoggerFactory.getLogger(EndlessTarpitSenderSupportDevelopment.class); + private final AtomicInteger lastCount = new AtomicInteger(); + + @Override + public void sessionEstablished(Session session) { + log.info("sessionEstablished({})", session); + } + + @Override + public void sessionPeerIdentificationLine( + Session session, String line, List<String> extraLines) { + if (lastCount.get() < GenericUtils.size(extraLines)) { + int num = lastCount.incrementAndGet(); + log.info("sessionPeerIdentificationLine({})[{}] {}", session, num, line); + } + } + + }); + + client.start(); + Duration waitTime = Duration.ofMinutes(15L); + try (ClientSession session = client.connect(host, host, port) + .verify(waitTime) + .getSession()) { + session.addPasswordIdentity(host); + session.auth().verify(waitTime); + } finally { + client.stop(); + } + } + } + + // optional args[0]=client/server - default=server, optional args[1]=port (default 22), optional args[2]=listen/connect address (default=localhost) + public static void main(String[] args) throws Exception { + int numArgs = GenericUtils.length(args); + String mode = (numArgs > 0) ? args[0] : "server"; + int port = (numArgs > 1) ? Integer.parseInt(args[1]) : SshConstants.DEFAULT_PORT; + if ("server".equalsIgnoreCase(mode)) { + startServer((numArgs > 2) ? args[2] : null, port); + } else { + startClient((numArgs > 2) ? args[2] : BaseTestSupport.TEST_LOCALHOST, port); + } + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java index 3bab23a..b81ac20 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java @@ -556,7 +556,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C } @Override - protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException { + protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception { mergeProposals(clientProposal, proposal); return super.sendKexInit(proposal); } @@ -683,10 +683,13 @@ public abstract class AbstractClientSession extends AbstractSession implements C proposal.put(KexProposalOption.C2SENC, BuiltinCiphers.Constants.NONE); proposal.put(KexProposalOption.S2CENC, BuiltinCiphers.Constants.NONE); - byte[] seed; - synchronized (kexState) { - seed = sendKexInit(proposal); - setKexSeed(seed); + try { + synchronized (kexState) { + byte[] seed = sendKexInit(proposal); + setKexSeed(seed); + } + } catch (Exception e) { + GenericUtils.rethrowAsIoException(e); } } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java index 72c41ae..8367583 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java @@ -225,7 +225,7 @@ public class ClientSessionImpl extends AbstractClientSession { } @Override - protected void signalSessionEvent(SessionListener.Event event) throws IOException { + protected void signalSessionEvent(SessionListener.Event event) throws Exception { if (SessionListener.Event.KeyEstablished.equals(event)) { sendInitialServiceRequest(); } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java index ae039f8..745921a 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java @@ -80,16 +80,16 @@ public interface KexExtensionHandler { * method is called during the negotiation phase even if {@code isKexExtensionsAvailable} returns {@code false} for * the session. * - * @param session The {@link Session} initiating or receiving the proposal - * @param initiator {@code true} if the proposal is about to be sent, {@code false} if this is a proposal - * received from the peer. - * @param proposal The proposal contents - <B>Caveat emptor:</B> the proposal is <U>modifiable</U> i.e., the - * handler can modify it before being sent or before being processed (if incoming) - * @throws IOException If failed to handle the request + * @param session The {@link Session} initiating or receiving the proposal + * @param initiator {@code true} if the proposal is about to be sent, {@code false} if this is a proposal received + * from the peer. + * @param proposal The proposal contents - <B>Caveat emptor:</B> the proposal is <U>modifiable</U> i.e., the + * handler can modify it before being sent or before being processed (if incoming) + * @throws Exception If failed to handle the request */ default void handleKexInitProposal( Session session, boolean initiator, Map<KexProposalOption, String> proposal) - throws IOException { + throws Exception { // ignored } @@ -98,20 +98,20 @@ public interface KexExtensionHandler { * called during the negotiation phase even if {@code isKexExtensionsAvailable} returns {@code false} for the * session. * - * @param session The {@link Session} executing the negotiation - * @param option The negotiated {@link KexProposalOption} - * @param nValue The negotiated option value (may be {@code null}/empty). - * @param c2sOptions The client proposals - * @param cValue The client-side value for the option (may be {@code null}/empty). - * @param s2cOptions The server proposals - * @param sValue The server-side value for the option (may be {@code null}/empty). - * @throws IOException If failed to handle the invocation + * @param session The {@link Session} executing the negotiation + * @param option The negotiated {@link KexProposalOption} + * @param nValue The negotiated option value (may be {@code null}/empty). + * @param c2sOptions The client proposals + * @param cValue The client-side value for the option (may be {@code null}/empty). + * @param s2cOptions The server proposals + * @param sValue The server-side value for the option (may be {@code null}/empty). + * @throws Exception If failed to handle the invocation */ default void handleKexExtensionNegotiation( Session session, KexProposalOption option, String nValue, Map<KexProposalOption, String> c2sOptions, String cValue, Map<KexProposalOption, String> s2cOptions, String sValue) - throws IOException { + throws Exception { // do nothing } @@ -131,12 +131,12 @@ public interface KexExtensionHandler { * Invoked in order to allow the handler to send an {@code SSH_MSG_EXT_INFO} message. <B>Note:</B> this method is * called only if {@code isKexExtensionsAvailable} returns {@code true} for the session. * - * @param session The {@link Session} - * @param phase The phase at which the handler is invoked - * @throws IOException If failed to handle the invocation - * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4">RFC-8308 - section 2.4</A> + * @param session The {@link Session} + * @param phase The phase at which the handler is invoked + * @throws Exception If failed to handle the invocation + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.4">RFC-8308 - section 2.4</A> */ - default void sendKexExtensions(Session session, KexPhase phase) throws IOException { + default void sendKexExtensions(Session session, KexPhase phase) throws Exception { // do nothing } @@ -144,15 +144,15 @@ public interface KexExtensionHandler { * Parses the {@code SSH_MSG_EXT_INFO} message. <B>Note:</B> this method is called regardless of whether * {@code isKexExtensionsAvailable} returns {@code true} for the session. * - * @param session The {@link Session} through which the message was received - * @param buffer The message buffer - * @return {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be - * generated - * @throws IOException If failed to handle the message - * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A> - * @see #handleKexExtensionRequest(Session, int, int, String, byte[]) + * @param session The {@link Session} through which the message was received + * @param buffer The message buffer + * @return {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be + * generated + * @throws Exception If failed to handle the message + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-2.3">RFC-8308 - section 2.3</A> + * @see #handleKexExtensionRequest(Session, int, int, String, byte[]) */ - default boolean handleKexExtensionsMessage(Session session, Buffer buffer) throws IOException { + default boolean handleKexExtensionsMessage(Session session, Buffer buffer) throws Exception { int count = buffer.getInt(); for (int index = 0; index < count; index++) { String name = buffer.getString(); @@ -169,31 +169,31 @@ public interface KexExtensionHandler { * Parses the {@code SSH_MSG_NEWCOMPRESS} message. <B>Note:</B> this method is called regardless of whether * {@code isKexExtensionsAvailable} returns {@code true} for the session. * - * @param session The {@link Session} through which the message was received - * @param buffer The message buffer - * @return {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be - * generated - * @throws IOException If failed to handle the message - * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A> + * @param session The {@link Session} through which the message was received + * @param buffer The message buffer + * @return {@code true} if message handled - if {@code false} then {@code SSH_MSG_UNIMPLEMENTED} will be + * generated + * @throws Exception If failed to handle the message + * @see <A HREF="https://tools.ietf.org/html/rfc8308#section-3.2">RFC-8308 - section 3.2</A> */ - default boolean handleKexCompressionMessage(Session session, Buffer buffer) throws IOException { + default boolean handleKexCompressionMessage(Session session, Buffer buffer) throws Exception { return true; } /** * Invoked by {@link #handleKexExtensionsMessage(Session, Buffer)} in order to handle a specific extension. * - * @param session The {@link Session} through which the message was received - * @param index The 0-based extension index - * @param count The total extensions in the message - * @param name The extension name - * @param data The extension data - * @return {@code true} whether to proceed to the next extension or stop processing the rest - * @throws IOException If failed to handle the extension + * @param session The {@link Session} through which the message was received + * @param index The 0-based extension index + * @param count The total extensions in the message + * @param name The extension name + * @param data The extension data + * @return {@code true} whether to proceed to the next extension or stop processing the rest + * @throws Exception If failed to handle the extension */ default boolean handleKexExtensionRequest( Session session, int index, int count, String name, byte[] data) - throws IOException { + throws Exception { return true; } } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java index ecc49f1..496f137 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java @@ -20,8 +20,10 @@ package org.apache.sshd.common.session; import java.util.List; +import java.util.Map; import org.apache.sshd.common.io.IoWriteFuture; +import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.util.SshdEventListener; import org.apache.sshd.common.util.buffer.Buffer; @@ -45,7 +47,7 @@ public interface ReservedSessionMessagesHandler extends SshdEventListener { * @return A {@link IoWriteFuture} that can be used to wait for the data to be sent successfully. If * {@code null} then the session will send the identification, otherwise it is assumed that the * handler has sent it. - * @throws Exception + * @throws Exception if failed to handle the callback * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2 - Protocol * Version Exchange</A> */ @@ -56,6 +58,23 @@ public interface ReservedSessionMessagesHandler extends SshdEventListener { } /** + * Invoked before sending the {@code SSH_MSG_KEXINIT} packet + * + * @param session The {@code Session} through which the key exchange is being managed + * @param proposal The KEX proposal that was used to build the packet + * @param packet The packet containing the fully encoded message - <B>Caveat:</B> this packet later serves as + * part of the key generation, so care must be taken if manipulating it. + * @return A non-{@code null} {@link IoWriteFuture} to signal that handler took care of the KEX packet + * delivery. + * @throws Exception if failed to handle the callback + */ + default IoWriteFuture sendKexInitRequest( + Session session, Map<KexProposalOption, String> proposal, Buffer packet) + throws Exception { + return null; + } + + /** * Invoked when an {@code SSH_MSG_IGNORE} packet is received * * @param session The {@code Session} through which the message was received diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java index 00815a2..5abca14 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionWorkBuffer.java @@ -21,6 +21,7 @@ package org.apache.sshd.common.session; import java.util.Objects; +import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; /** @@ -39,7 +40,7 @@ public class SessionWorkBuffer extends ByteArrayBuffer implements SessionHolder< } @Override - public void clear(boolean wipeData) { + public Buffer clear(boolean wipeData) { throw new UnsupportedOperationException("Not allowed to clear session work buffer of " + getSession()); } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java index 13bb2f9..6a2b146 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java @@ -376,7 +376,7 @@ public abstract class AbstractSession extends SessionHelper { public void messageReceived(Readable buffer) throws Exception { synchronized (decodeLock) { decoderBuffer.putBuffer(buffer); - // One of those property will be set by the constructor and the other + // One of those properties will be set by the constructor and the other // one should be set by the readIdentification method if ((clientVersion == null) || (serverVersion == null)) { if (readIdentification(decoderBuffer)) { @@ -568,10 +568,10 @@ public abstract class AbstractSession extends SessionHelper { /** * Send a message to put new keys into use. * - * @return An {@link IoWriteFuture} that can be used to wait and check the result of sending the packet - * @throws IOException if an error occurs sending the message + * @return An {@link IoWriteFuture} that can be used to wait and check the result of sending the packet + * @throws Exception if an error occurs sending the message */ - protected IoWriteFuture sendNewKeys() throws IOException { + protected IoWriteFuture sendNewKeys() throws Exception { if (log.isDebugEnabled()) { log.debug("sendNewKeys({}) Send SSH_MSG_NEWKEYS", this); } @@ -897,6 +897,8 @@ public abstract class AbstractSession extends SessionHelper { "Failed (" + e.getClass().getSimpleName() + ")" + " to check re-key necessity: " + e.getMessage()), e); + } catch (Exception e) { + GenericUtils.rethrowAsIoException(e); } } } @@ -1507,14 +1509,16 @@ public abstract class AbstractSession extends SessionHelper { /** * Send the key exchange initialization packet. This packet contains random data along with our proposal. * - * @param proposal our proposal for key exchange negotiation - * @return the sent packet data which must be kept for later use when deriving the session keys - * @throws IOException if an error occurred sending the packet + * @param proposal our proposal for key exchange negotiation + * @return the sent packet data which must be kept for later use when deriving the session keys + * @throws Exception if an error occurred sending the packet */ - protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException { - if (log.isDebugEnabled()) { + protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception { + boolean debugEnabled = log.isDebugEnabled(); + if (debugEnabled) { log.debug("sendKexInit({}) Send SSH_MSG_KEXINIT", this); } + Buffer buffer = createBuffer(SshConstants.SSH_MSG_KEXINIT); int p = buffer.wpos(); buffer.wpos(p + SshConstants.MSG_KEX_COOKIE_SIZE); @@ -1538,20 +1542,30 @@ public abstract class AbstractSession extends SessionHelper { buffer.putBoolean(false); // first kex packet follows buffer.putInt(0); // reserved (FFU) + + ReservedSessionMessagesHandler handler = getReservedSessionMessagesHandler(); + IoWriteFuture future = (handler == null) ? null : handler.sendKexInitRequest(this, proposal, buffer); byte[] data = buffer.getCompactData(); - writePacket(buffer); + if (future == null) { + future = writePacket(buffer); + } else { + if (debugEnabled) { + log.debug("sendKexInit({}) KEX handled by reserved messages handler", this); + } + } + return data; } /** * Receive the remote key exchange init message. The packet data is returned for later use. * - * @param buffer the {@link Buffer} containing the key exchange init packet - * @param proposal the remote proposal to fill - * @return the packet data - * @throws IOException If failed to handle the message + * @param buffer the {@link Buffer} containing the key exchange init packet + * @param proposal the remote proposal to fill + * @return the packet data + * @throws Exception If failed to handle the message */ - protected byte[] receiveKexInit(Buffer buffer, Map<KexProposalOption, String> proposal) throws IOException { + protected byte[] receiveKexInit(Buffer buffer, Map<KexProposalOption, String> proposal) throws Exception { // Recreate the packet payload which will be needed at a later time byte[] d = buffer.array(); byte[] data = new byte[buffer.available() + 1 /* the opcode */]; @@ -1791,10 +1805,10 @@ public abstract class AbstractSession extends SessionHelper { * Compute the negotiated proposals by merging the client and server proposal. The negotiated proposal will also be * stored in the {@link #negotiationResult} property. * - * @return The negotiated options {@link Map} - * @throws IOException If negotiation failed + * @return The negotiated options {@link Map} + * @throws Exception If negotiation failed */ - protected Map<KexProposalOption, String> negotiate() throws IOException { + protected Map<KexProposalOption, String> negotiate() throws Exception { Map<KexProposalOption, String> c2sOptions = getClientKexProposals(); Map<KexProposalOption, String> s2cOptions = getServerKexProposals(); signalNegotiationStart(c2sOptions, s2cOptions); @@ -2104,6 +2118,9 @@ public abstract class AbstractSession extends SessionHelper { "Failed (" + e.getClass().getSimpleName() + ")" + " to generate keys for exchange: " + e.getMessage()), e); + } catch (Exception e) { + GenericUtils.rethrowAsIoException(e); + return null; // actually dead code } return ValidateUtils.checkNotNull( @@ -2113,26 +2130,24 @@ public abstract class AbstractSession extends SessionHelper { /** * Checks if a re-keying is required and if so initiates it * - * @return A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} - * if no need to re-key or an exchange is already in progress - * @throws IOException If failed load the keys or send the request - * @throws GeneralSecurityException If failed to generate the necessary keys - * @see #isRekeyRequired() - * @see #requestNewKeysExchange() + * @return A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} if no need to + * re-key or an exchange is already in progress + * @throws Exception If failed load/generate the keys or send the request + * @see #isRekeyRequired() + * @see #requestNewKeysExchange() */ - protected KeyExchangeFuture checkRekey() throws IOException, GeneralSecurityException { + protected KeyExchangeFuture checkRekey() throws Exception { return isRekeyRequired() ? requestNewKeysExchange() : null; } /** * Initiates a new keys exchange if one not already in progress * - * @return A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} - * if an exchange is already in progress - * @throws IOException If failed to load the keys or send the request - * @throws GeneralSecurityException If failed to generate the keys + * @return A {@link KeyExchangeFuture} to wait for the initiated exchange or {@code null} if an exchange + * is already in progress + * @throws Exception If failed to load/generate the keys or send the request */ - protected KeyExchangeFuture requestNewKeysExchange() throws IOException, GeneralSecurityException { + protected KeyExchangeFuture requestNewKeysExchange() throws Exception { if (!kexState.compareAndSet(KexState.DONE, KexState.INIT)) { if (log.isDebugEnabled()) { log.debug("requestNewKeysExchange({}) KEX state not DONE: {}", this, kexState); @@ -2259,7 +2274,7 @@ public abstract class AbstractSession extends SessionHelper { } } - protected byte[] sendKexInit() throws IOException, GeneralSecurityException { + protected byte[] sendKexInit() throws Exception { String resolvedAlgorithms = resolveAvailableSignaturesProposal(); if (GenericUtils.isEmpty(resolvedAlgorithms)) { throw new SshException( diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java index a5d7542..4de89bf 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java @@ -214,7 +214,11 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements @Override public void setAuthenticated() throws IOException { this.authed = true; - signalSessionEvent(SessionListener.Event.Authenticated); + try { + signalSessionEvent(SessionListener.Event.Authenticated); + } catch (Exception e) { + GenericUtils.rethrowAsIoException(e); + } } /** @@ -691,10 +695,10 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements /** * Sends a session event to all currently registered session listeners * - * @param event The event to send - * @throws IOException If any of the registered listeners threw an exception. + * @param event The event to send + * @throws Exception If any of the registered listeners threw an exception. */ - protected void signalSessionEvent(SessionListener.Event event) throws IOException { + protected void signalSessionEvent(SessionListener.Event event) throws Exception { try { invokeSessionSignaller(l -> { signalSessionEvent(l, event); @@ -704,13 +708,10 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements Throwable t = GenericUtils.peelException(err); debug("sendSessionEvent({})[{}] failed ({}) to inform listeners: {}", this, event, t.getClass().getSimpleName(), t.getMessage(), t); - if (t instanceof IOException) { - throw (IOException) t; - } else if (t instanceof RuntimeException) { - throw (RuntimeException) t; + if (t instanceof Exception) { + throw (Exception) t; } else { - throw new IOException( - "Failed (" + t.getClass().getSimpleName() + ") to send session event: " + t.getMessage(), t); + throw new RuntimeSshException(t); } } } @@ -1279,6 +1280,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements Throwable e = GenericUtils.peelException(err); debug("signalSessionClosed({}) {} while signal session closed: {}", this, e.getClass().getSimpleName(), e.getMessage(), e); + // Do not re-throw since session closed anyway } } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java index b388a6c..5cff5f9 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java @@ -354,7 +354,7 @@ public abstract class AbstractServerSession extends AbstractSession implements S } @Override - protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException { + protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws Exception { mergeProposals(serverProposal, proposal); return super.sendKexInit(proposal); } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java index 0b4a588..ea99c9b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java @@ -39,7 +39,8 @@ public class ServerSessionImpl extends AbstractServerSession { String headerConfig = CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES.getOrNull(this); String[] headers = GenericUtils.split(headerConfig, CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR); - // We intentionally create a modifiable array so as to allow users to modify it via SessionListener or ReservedSessionMessagesHandler + // We intentionally create a modifiable array so as to allow users to + // modify it via SessionListener or ReservedSessionMessagesHandler List<String> extraLines = GenericUtils.isEmpty(headers) ? new ArrayList<>() : new ArrayList<>(Arrays.asList(headers)); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java index d38c817..307cccc 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/KexExtensionHandlerTest.java @@ -53,7 +53,7 @@ public class KexExtensionHandlerTest extends JUnitTestSupport { } @Test - public void testEncodeDecodeExtensionMessage() throws IOException { + public void testEncodeDecodeExtensionMessage() throws Exception { List<Map.Entry<String, ?>> expected = Arrays.asList( new SimpleImmutableEntry<>( DelayCompression.NAME, diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java b/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java index 6e52784..686d2de 100644 --- a/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java +++ b/sshd-core/src/test/java/org/apache/sshd/util/test/CoreTestSupportUtils.java @@ -56,7 +56,10 @@ public final class CoreTestSupportUtils { } public static SshClient setupTestClient(Class<?> anchor) { - SshClient client = SshClient.setUpDefaultClient(); + return setupTestClient(SshClient.setUpDefaultClient(), anchor); + } + + public static <C extends SshClient> C setupTestClient(C client, Class<?> anchor) { client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE); client.setHostConfigEntryResolver(HostConfigEntryResolver.EMPTY); client.setKeyIdentityProvider(KeyIdentityProvider.EMPTY_KEYS_PROVIDER); @@ -77,7 +80,10 @@ public final class CoreTestSupportUtils { } public static SshServer setupTestServer(Class<?> anchor) { - SshServer sshd = SshServer.setUpDefaultServer(); + return setupTestServer(SshServer.setUpDefaultServer(), anchor); + } + + public static <S extends SshServer> S setupTestServer(S sshd, Class<?> anchor) { sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(anchor)); sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE); sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java index 7b8e474..54b865f 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/SimpleScpClientImpl.java @@ -109,7 +109,8 @@ public class SimpleScpClientImpl extends AbstractLoggingBean implements SimpleSc e.addSuppressed(t); } - throw GenericUtils.toIOException(e); + GenericUtils.rethrowAsIoException(e); + return null; // actually dead code... } }