From 9461336639e218d7216ce21c11706d982674f7c2 Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 24 Sep 2023 17:16:21 +0800 Subject: [PATCH] fix code --- .../dromara/hutool/extra/ssh/Connector.java | 42 ++++- .../ssh/engine/ganymed/GanymedSession.java | 148 ++++++++++++++---- .../extra/ssh/engine/ganymed/GanymedUtil.java | 73 +++++++++ .../extra/ssh/engine/jsch/JschSession.java | 148 +++++++++--------- .../engine/jsch/{Sftp.java => JschSftp.java} | 148 ++++++------------ .../extra/ssh/engine/jsch/JschUtil.java | 117 ++++++++++++++ .../extra/ssh/engine/sshj/SshjSession.java | 28 +++- .../extra/ssh/engine/sshj/SshjUtil.java | 50 ++++++ .../org/dromara/hutool/extra/ftp/FtpTest.java | 6 +- .../ssh/{JschUtilTest.java => JschTest.java} | 52 +++--- 10 files changed, 568 insertions(+), 244 deletions(-) create mode 100644 hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedUtil.java rename hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/{Sftp.java => JschSftp.java} (83%) create mode 100644 hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschUtil.java create mode 100644 hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjUtil.java rename hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/{JschUtilTest.java => JschTest.java} (58%) diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/Connector.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/Connector.java index d7b712f3d..fddfcd22f 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/Connector.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/Connector.java @@ -23,6 +23,7 @@ public class Connector { private String user = "root"; private String password; private String group; + private long timeout; /** * 构造 @@ -52,10 +53,24 @@ public class Connector { * @param password 密码 */ public Connector(final String host, final int port, final String user, final String password) { + this(host, port, user, password, 0); + } + + /** + * 构造 + * + * @param host 主机名 + * @param port 端口 + * @param user 用户名 + * @param password 密码 + * @param timeout 连接超时时长,0表示默认 + */ + public Connector(final String host, final int port, final String user, final String password, final long timeout) { this.host = host; this.port = port; this.user = user; this.password = password; + this.timeout = timeout; } /** @@ -148,11 +163,36 @@ public class Connector { this.group = group; } + /** + * 获得连接超时时间 + * + * @return 连接超时时间 + */ + public long getTimeout() { + return timeout; + } + + /** + * 设置连接超时时间 + * + * @param timeout 连接超时时间 + */ + public void setTimeout(final long timeout) { + this.timeout = timeout; + } + /** * toString方法仅用于测试显示 */ @Override public String toString() { - return "Connector [host=" + host + ", port=" + port + ", user=" + user + ", password=" + password + "]"; + return "Connector{" + + "host='" + host + '\'' + + ", port=" + port + + ", user='" + user + '\'' + + ", password='" + password + '\'' + + ", group='" + group + '\'' + + ", timeout=" + timeout + + '}'; } } diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedSession.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedSession.java index 86474114d..01acb98cb 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedSession.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedSession.java @@ -13,16 +13,22 @@ package org.dromara.hutool.extra.ssh.engine.ganymed; import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.LocalPortForwarder; import ch.ethz.ssh2.StreamGobbler; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; +import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.core.net.Ipv4Util; import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.extra.ssh.Connector; import org.dromara.hutool.extra.ssh.Session; import java.io.IOException; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; /** * {@link ch.ethz.ssh2.Session}包装 @@ -31,15 +37,28 @@ import java.nio.charset.Charset; */ public class GanymedSession implements Session { + private Connection connection; private final ch.ethz.ssh2.Session raw; + private Map localPortForwarderMap; + /** * 构造 * * @param connector {@link Connector},保存连接和验证信息等 */ public GanymedSession(final Connector connector) { - this(openSession(connector)); + this(GanymedUtil.openConnection(connector)); + } + + /** + * 构造 + * + * @param connection {@link Connection},连接对象 + */ + public GanymedSession(final Connection connection) { + this(GanymedUtil.openSession(connection)); + this.connection = connection; } /** @@ -47,7 +66,7 @@ public class GanymedSession implements Session { * * @param raw {@link ch.ethz.ssh2.Session} */ - public GanymedSession(final ch.ethz.ssh2.Session raw) { + private GanymedSession(final ch.ethz.ssh2.Session raw) { this.raw = raw; } @@ -61,6 +80,104 @@ public class GanymedSession implements Session { if (raw != null) { raw.close(); } + if (connection != null) { + connection.close(); + } + } + + /** + * 绑定端口到本地。 一个会话可绑定多个端口 + * + * @param remoteHost 远程主机 + * @param remotePort 远程端口 + * @param localPort 本地端口 + * @return 成功与否 + * @throws IORuntimeException 端口绑定失败异常 + */ + public boolean bindLocalPort(final String remoteHost, final int remotePort, final int localPort) throws IORuntimeException { + return bindLocalPort(remoteHost, remotePort, Ipv4Util.LOCAL_IP, localPort); + } + + /** + * 绑定端口到本地。 一个会话可绑定多个端口 + * + * @param remoteHost 远程主机 + * @param remotePort 远程端口 + * @param localHost 本地主机 + * @param localPort 本地端口 + * @return 成功与否 + * @throws IORuntimeException 端口绑定失败异常 + */ + public boolean bindLocalPort(final String remoteHost, final int remotePort, final String localHost, final int localPort) throws IORuntimeException { + final LocalPortForwarder localPortForwarder; + try { + localPortForwarder = this.connection.createLocalPortForwarder(new InetSocketAddress(localHost, localPort), remoteHost, remotePort); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + + if(null == this.localPortForwarderMap){ + this.localPortForwarderMap = new HashMap<>(); + } + + //加入记录 + this.localPortForwarderMap.put(localPort, localPortForwarder); + + return true; + } + + /** + * 解除本地端口映射 + * + * @param localPort 需要解除的本地端口 + * @throws IORuntimeException 端口解绑失败异常 + */ + public void unBindLocalPort(final int localPort) throws IORuntimeException { + if(MapUtil.isEmpty(this.localPortForwarderMap)){ + return; + } + + final LocalPortForwarder localPortForwarder = this.localPortForwarderMap.remove(localPort); + if(null != localPortForwarder){ + try { + localPortForwarder.close(); + } catch (final IOException e) { + // ignore + } + } + } + + /** + * 绑定ssh服务端的serverPort端口, 到host主机的port端口上.
+ * 即数据从ssh服务端的serverPort端口, 流经ssh客户端, 达到host:port上. + * + * @param bindPort ssh服务端上要被绑定的端口 + * @param host 转发到的host + * @param port host上的端口 + * @return 成功与否 + * @throws IORuntimeException 端口绑定失败异常 + */ + public boolean bindRemotePort(final int bindPort, final String host, final int port) throws IORuntimeException { + try { + this.connection.requestRemotePortForwarding(Ipv4Util.LOCAL_IP, bindPort, host, port); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + return true; + } + + /** + * 解除远程端口映射 + * + * @param localPort 需要解除的本地端口 + * @throws IORuntimeException 端口解绑失败异常 + */ + public void unBindRemotePort(final int localPort) throws IORuntimeException { + try { + this.connection.cancelRemotePortForwarding(localPort); + } catch (final IOException e) { + throw new IORuntimeException(e); + } } /** @@ -87,7 +204,7 @@ public class GanymedSession implements Session { } // 错误输出 - if(null != errStream){ + if (null != errStream) { IoUtil.copy(new StreamGobbler(this.raw.getStderr()), errStream); } @@ -128,29 +245,4 @@ public class GanymedSession implements Session { // 结果输出 return IoUtil.read(new StreamGobbler(this.raw.getStdout()), charset); } - - /** - * 初始化并打开新的Session - * - * @param connector {@link Connector},保存连接和验证信息等 - * @return {@link ch.ethz.ssh2.Session} - */ - private static ch.ethz.ssh2.Session openSession(final Connector connector) { - - // 建立连接 - final Connection conn = new Connection(connector.getHost(), connector.getPort()); - try { - conn.connect(); - } catch (final IOException e) { - throw new IORuntimeException(e); - } - - // 打开会话 - try { - conn.authenticateWithPassword(connector.getUser(), connector.getPassword()); - return conn.openSession(); - } catch (final IOException e) { - throw new IORuntimeException(e); - } - } } diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedUtil.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedUtil.java new file mode 100644 index 000000000..6cfc666da --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/ganymed/GanymedUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.extra.ssh.engine.ganymed; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.Session; +import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.extra.ssh.Connector; + +import java.io.IOException; + +/** + * Ganymed-ssh2相关工具类 + * + * @author looly + */ +public class GanymedUtil { + + /** + * 打开SSH连接 + * + * @param connector 连接信息 + * @return {@link Connection} + */ + public static Connection openConnection(final Connector connector) { + // 建立连接 + final Connection conn = new Connection(connector.getHost(), connector.getPort()); + try { + conn.connect(); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + + // 验证 + final boolean isAuth; + try { + isAuth = conn.authenticateWithPassword(connector.getUser(), connector.getPassword()); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + + if(!isAuth){ + throw new IORuntimeException("Authentication failed."); + } + + return conn; + } + + /** + * 打开SSH会话 + * + * @param connection 连接对象 + * @return {@link Session} + */ + public static Session openSession(final Connection connection) { + // 打开会话 + try { + return connection.openSession(); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSession.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSession.java index 31677124c..9129a35f6 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSession.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSession.java @@ -15,6 +15,7 @@ package org.dromara.hutool.extra.ssh.engine.jsch; import com.jcraft.jsch.*; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; +import org.dromara.hutool.core.net.Ipv4Util; import org.dromara.hutool.core.util.ByteUtil; import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.extra.ssh.Connector; @@ -32,6 +33,7 @@ import java.nio.charset.Charset; public class JschSession implements Session { private final com.jcraft.jsch.Session raw; + private final long timeout; /** * 构造 @@ -39,28 +41,51 @@ public class JschSession implements Session { * @param connector {@link Connector},保存连接和验证信息等 */ public JschSession(final Connector connector) { - this(openSession(connector)); + this(JschUtil.openSession(connector), connector.getTimeout()); } /** * 构造 * - * @param raw {@link com.jcraft.jsch.Session} + * @param raw {@link com.jcraft.jsch.Session} + * @param timeout 连接超时时常,0表示不限制 */ - public JschSession(final com.jcraft.jsch.Session raw) { + public JschSession(final com.jcraft.jsch.Session raw, final long timeout) { this.raw = raw; + this.timeout = timeout; } @Override - public Object getRaw() { - return raw; + public com.jcraft.jsch.Session getRaw() { + return this.raw; + } + + /** + * 是否连接状态 + * + * @return 是否连接状态 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isConnected() { + return null != this.raw && this.raw.isConnected(); } @Override public void close() throws IOException { - if (raw != null && raw.isConnected()) { - raw.disconnect(); - } + JschUtil.close(this.raw); + } + + /** + * 绑定端口到本地。 一个会话可绑定多个端口 + * + * @param remoteHost 远程主机 + * @param remotePort 远程端口 + * @param localPort 本地端口 + * @return 成功与否 + * @throws SshException 端口绑定失败异常 + */ + public boolean bindLocalPort(final String remoteHost, final int remotePort, final int localPort) throws SshException { + return bindLocalPort(remoteHost, remotePort, Ipv4Util.LOCAL_IP, localPort); } /** @@ -73,16 +98,30 @@ public class JschSession implements Session { * @return 成功与否 * @throws SshException 端口绑定失败异常 */ - public boolean bindPort(final String remoteHost, final int remotePort, final String localHost, final int localPort) throws SshException { - if (this.raw != null && this.raw.isConnected()) { - try { - this.raw.setPortForwardingL(localHost, localPort, remoteHost, remotePort); - } catch (final JSchException e) { - throw new SshException(e, "From [{}:{}] mapping to [{}:{}] error!", remoteHost, remotePort, localHost, localPort); - } - return true; + public boolean bindLocalPort(final String remoteHost, final int remotePort, final String localHost, final int localPort) throws SshException { + if (!isConnected()) { + return false; + } + + try { + this.raw.setPortForwardingL(localHost, localPort, remoteHost, remotePort); + } catch (final JSchException e) { + throw new SshException(e, "From [{}:{}] mapping to [{}:{}] error!", remoteHost, remotePort, localHost, localPort); + } + return true; + } + + /** + * 解除远程端口映射 + * + * @param localPort 需要解除的本地端口 + */ + public void unBindLocalPort(final int localPort) { + try { + this.raw.delPortForwardingL(localPort); + } catch (final JSchException e) { + throw new SshException(e); } - return false; } /** @@ -96,25 +135,26 @@ public class JschSession implements Session { * @throws SshException 端口绑定失败异常 */ public boolean bindRemotePort(final int bindPort, final String host, final int port) throws SshException { - if (this.raw != null && this.raw.isConnected()) { - try { - this.raw.setPortForwardingR(bindPort, host, port); - } catch (final JSchException e) { - throw new SshException(e, "From [{}] mapping to [{}] error!", bindPort, port); - } - return true; + if (!isConnected()) { + return false; } - return false; + + try { + this.raw.setPortForwardingR(bindPort, host, port); + } catch (final JSchException e) { + throw new SshException(e, "From [{}] mapping to [{}] error!", bindPort, port); + } + return true; } /** - * 解除端口映射 + * 解除远程端口映射 * * @param localPort 需要解除的本地端口 */ - public void unBindPort(final int localPort) { + public void unBindRemotePort(final int localPort) { try { - this.raw.delPortForwardingL(localPort); + this.raw.delPortForwardingR(localPort); } catch (final JSchException e) { throw new SshException(e); } @@ -127,16 +167,7 @@ public class JschSession implements Session { * @return {@link Channel} */ public Channel createChannel(final ChannelType channelType) { - final Channel channel; - try { - if (!this.raw.isConnected()) { - this.raw.connect(); - } - channel = this.raw.openChannel(channelType.getValue()); - } catch (final JSchException e) { - throw new SshException(e); - } - return channel; + return JschUtil.createChannel(this.raw, channelType, this.timeout); } /** @@ -155,24 +186,17 @@ public class JschSession implements Session { * @return {@link Channel} */ public Channel openChannel(final ChannelType channelType) { - return openChannel(channelType, 0); + return JschUtil.openChannel(this.raw, channelType, this.timeout); } /** - * 打开Channel连接 + * 打开SFTP会话 * - * @param channelType 通道类型,可以是shell或sftp等,见{@link ChannelType} - * @param timeout 连接超时时长,单位毫秒 - * @return {@link Channel} + * @param charset 编码 + * @return {@link JschSftp} */ - public Channel openChannel(final ChannelType channelType, final int timeout) { - final Channel channel = createChannel(channelType); - try { - channel.connect(Math.max(timeout, 0)); - } catch (final JSchException e) { - throw new SshException(e); - } - return channel; + public JschSftp openSftp(final Charset charset) { + return new JschSftp(this.raw, charset, this.timeout); } /** @@ -259,26 +283,4 @@ public class JschSession implements Session { } } } - - /** - * 创建{@link com.jcraft.jsch.Session} - * - * @param connector {@link Connector},保存连接和验证信息等 - * @return {@link com.jcraft.jsch.Session} - */ - private static com.jcraft.jsch.Session openSession(final Connector connector) { - final JSch jsch = new JSch(); - final com.jcraft.jsch.Session session; - try { - session = jsch.getSession(connector.getUser(), connector.getHost(), connector.getPort()); - } catch (final JSchException e) { - throw new SshException(e); - } - - session.setPassword(connector.getPassword()); - // 设置第一次登录的时候提示,可选值:(ask | yes | no) - session.setConfig("StrictHostKeyChecking", "no"); - - return session; - } } diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/Sftp.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSftp.java similarity index 83% rename from hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/Sftp.java rename to hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSftp.java index e2f543809..1ffd3e869 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/Sftp.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschSftp.java @@ -12,6 +12,7 @@ package org.dromara.hutool.extra.ssh.engine.jsch; +import com.jcraft.jsch.*; import org.dromara.hutool.core.collection.CollUtil; import org.dromara.hutool.core.collection.ListUtil; import org.dromara.hutool.core.io.file.FileUtil; @@ -19,13 +20,9 @@ import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.extra.ftp.AbstractFtp; import org.dromara.hutool.extra.ftp.FtpConfig; import org.dromara.hutool.extra.ftp.FtpException; -import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.ChannelSftp.LsEntry; import com.jcraft.jsch.ChannelSftp.LsEntrySelector; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; -import com.jcraft.jsch.SftpProgressMonitor; +import org.dromara.hutool.extra.ssh.Connector; import org.dromara.hutool.extra.ssh.SshException; import java.io.File; @@ -50,7 +47,7 @@ import java.util.function.Predicate; * @author looly * @since 4.0.2 */ -public class Sftp extends AbstractFtp { +public class JschSftp extends AbstractFtp { private Session session; private ChannelSftp channel; @@ -65,7 +62,7 @@ public class Sftp extends AbstractFtp { * @param sshUser 远程主机用户名 * @param sshPass 远程主机密码 */ - public Sftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass) { + public JschSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass) { this(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET); } @@ -79,7 +76,7 @@ public class Sftp extends AbstractFtp { * @param charset 编码 * @since 4.1.14 */ - public Sftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) { + public JschSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) { this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset)); } @@ -89,7 +86,7 @@ public class Sftp extends AbstractFtp { * @param config FTP配置 * @since 5.3.3 */ - public Sftp(final FtpConfig config) { + public JschSftp(final FtpConfig config) { this(config, true); } @@ -100,45 +97,13 @@ public class Sftp extends AbstractFtp { * @param init 是否立即初始化 * @since 5.8.4 */ - public Sftp(final FtpConfig config, final boolean init) { + public JschSftp(final FtpConfig config, final boolean init) { super(config); if (init) { - init(config); + init(); } } - /** - * 构造 - * - * @param session {@link Session} - */ - public Sftp(final Session session) { - this(session, DEFAULT_CHARSET); - } - - /** - * 构造 - * - * @param session {@link Session} - * @param charset 编码 - * @since 4.1.14 - */ - public Sftp(final Session session, final Charset charset) { - super(FtpConfig.of().setCharset(charset)); - init(session, charset); - } - - /** - * 构造 - * - * @param channel {@link ChannelSftp} - * @param charset 编码 - */ - public Sftp(final ChannelSftp channel, final Charset charset) { - super(FtpConfig.of().setCharset(charset)); - init(channel, charset); - } - /** * 构造 * @@ -147,9 +112,10 @@ public class Sftp extends AbstractFtp { * @param timeOut 超时时间,单位毫秒 * @since 5.8.4 */ - public Sftp(final Session session, final Charset charset, final long timeOut) { + public JschSftp(final Session session, final Charset charset, final long timeOut) { super(FtpConfig.of().setCharset(charset).setConnectionTimeout(timeOut)); - init(session, charset); + this.session = session; + init(); } /** @@ -160,73 +126,49 @@ public class Sftp extends AbstractFtp { * @param timeOut 超时时间,单位毫秒 * @since 5.8.4 */ - public Sftp(final ChannelSftp channel, final Charset charset, final long timeOut) { + public JschSftp(final ChannelSftp channel, final Charset charset, final long timeOut) { super(FtpConfig.of().setCharset(charset).setConnectionTimeout(timeOut)); - init(channel, charset); + this.channel = channel; + init(); } // ---------------------------------------------------------------------------------------- Constructor end - - /** - * 构造 - * - * @param sshHost 远程主机 - * @param sshPort 远程主机端口 - * @param sshUser 远程主机用户名 - * @param sshPass 远程主机密码 - * @param charset 编码 - */ - public void init(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) { - init(JschUtil.getSession(sshHost, sshPort, sshUser, sshPass), charset); - } - /** * 初始化 - * - * @since 5.3.3 */ + @SuppressWarnings("resource") public void init() { - init(this.ftpConfig); - } + if(null == this.channel){ + if(null == this.session){ + final FtpConfig config = this.ftpConfig; + this.session = new JschSession(new Connector( + config.getHost(), + config.getPort(), + config.getUser(), + config.getPassword(), + config.getConnectionTimeout())) + .getRaw(); + } - /** - * 初始化 - * - * @param config FTP配置 - * @since 5.3.3 - */ - public void init(final FtpConfig config) { - init(config.getHost(), config.getPort(), config.getUser(), config.getPassword(), config.getCharset()); - } + // 创建Channel + try { + this.channel = (ChannelSftp) this.session.openChannel(ChannelType.SFTP.getValue()); + } catch (final JSchException e) { + throw new SshException(e); + } + } - /** - * 初始化 - * - * @param session {@link Session} - * @param charset 编码 - */ - public void init(final Session session, final Charset charset) { - this.session = session; - init(JschUtil.openSftp(session, (int) this.ftpConfig.getConnectionTimeout()), charset); - } - - /** - * 初始化 - * - * @param channel {@link ChannelSftp} - * @param charset 编码 - */ - public void init(final ChannelSftp channel, final Charset charset) { - this.ftpConfig.setCharset(charset); try { - channel.setFilenameEncoding(charset.toString()); - } catch (final SftpException e) { + if(!channel.isConnected()){ + channel.connect((int) Math.max(this.ftpConfig.getConnectionTimeout(), 0)); + } + channel.setFilenameEncoding(this.ftpConfig.getCharset().toString()); + } catch (final JSchException | SftpException e) { throw new SshException(e); } - this.channel = channel; } @Override - public Sftp reconnectIfTimeout() { + public JschSftp reconnectIfTimeout() { if (StrUtil.isBlank(this.ftpConfig.getHost())) { throw new FtpException("Host is blank!"); } @@ -554,7 +496,7 @@ public class Sftp extends AbstractFtp { * @param destPath 目标路径, * @return this */ - public Sftp put(final String srcFilePath, final String destPath) { + public JschSftp put(final String srcFilePath, final String destPath) { return put(srcFilePath, destPath, Mode.OVERWRITE); } @@ -566,7 +508,7 @@ public class Sftp extends AbstractFtp { * @param mode {@link Mode} 模式 * @return this */ - public Sftp put(final String srcFilePath, final String destPath, final Mode mode) { + public JschSftp put(final String srcFilePath, final String destPath, final Mode mode) { return put(srcFilePath, destPath, null, mode); } @@ -580,7 +522,7 @@ public class Sftp extends AbstractFtp { * @return this * @since 4.6.5 */ - public Sftp put(final String srcFilePath, final String destPath, final SftpProgressMonitor monitor, final Mode mode) { + public JschSftp put(final String srcFilePath, final String destPath, final SftpProgressMonitor monitor, final Mode mode) { try { getClient().put(srcFilePath, destPath, monitor, mode.ordinal()); } catch (final SftpException e) { @@ -599,7 +541,7 @@ public class Sftp extends AbstractFtp { * @return this * @since 5.7.16 */ - public Sftp put(final InputStream srcStream, final String destPath, final SftpProgressMonitor monitor, final Mode mode) { + public JschSftp put(final InputStream srcStream, final String destPath, final SftpProgressMonitor monitor, final Mode mode) { try { getClient().put(srcStream, destPath, monitor, mode.ordinal()); } catch (final SftpException e) { @@ -662,7 +604,7 @@ public class Sftp extends AbstractFtp { * @param dest 目标文件路径 * @return this */ - public Sftp get(final String src, final String dest) { + public JschSftp get(final String src, final String dest) { try { getClient().get(src, dest); } catch (final SftpException e) { @@ -679,7 +621,7 @@ public class Sftp extends AbstractFtp { * @return this * @since 5.7.0 */ - public Sftp get(final String src, final OutputStream out) { + public JschSftp get(final String src, final OutputStream out) { try { getClient().get(src, out); } catch (final SftpException e) { diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschUtil.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschUtil.java new file mode 100644 index 000000000..4f69bb097 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/jsch/JschUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.extra.ssh.engine.jsch; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import org.dromara.hutool.extra.ssh.Connector; +import org.dromara.hutool.extra.ssh.SshException; + +/** + * Jsch工具类
+ * Jsch是Java Secure Channel的缩写。JSch是一个SSH2的纯Java实现。
+ * 它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等。
+ * + * @author Looly + * @since 4.0.0 + */ +public class JschUtil { + + /** + * 打开Session会话 + * @param connector 连接信息 + * @return {@link JschSession} + */ + public static Session openSession(final Connector connector){ + final JSch jsch = new JSch(); + final com.jcraft.jsch.Session session; + try { + session = jsch.getSession(connector.getUser(), connector.getHost(), connector.getPort()); + session.setTimeout((int) connector.getTimeout()); + } catch (final JSchException e) { + throw new SshException(e); + } + + session.setPassword(connector.getPassword()); + // 设置第一次登录的时候提示,可选值:(ask | yes | no) + session.setConfig("StrictHostKeyChecking", "no"); + + return session; + } + + /** + * 打开Channel连接 + * + * @param session Session会话 + * @param channelType 通道类型,可以是shell或sftp等,见{@link ChannelType} + * @param timeout 连接超时时长,单位毫秒 + * @return {@link Channel} + * @since 5.3.3 + */ + public static Channel openChannel(final Session session, final ChannelType channelType, final long timeout) { + final Channel channel = createChannel(session, channelType, timeout); + try { + channel.connect((int) Math.max(timeout, 0)); + } catch (final JSchException e) { + throw new SshException(e); + } + return channel; + } + + /** + * 创建Channel连接 + * + * @param session Session会话 + * @param channelType 通道类型,可以是shell或sftp等,见{@link ChannelType} + * @param timeout session超时时常,单位:毫秒 + * @return {@link Channel} + * @since 4.5.2 + */ + public static Channel createChannel(final Session session, final ChannelType channelType, final long timeout) { + final Channel channel; + try { + if (false == session.isConnected()) { + session.connect((int) timeout); + } + channel = session.openChannel(channelType.getValue()); + } catch (final JSchException e) { + throw new SshException(e); + } + return channel; + } + + /** + * 关闭SSH连接会话 + * + * @param session SSH会话 + */ + public static void close(final Session session) { + if (session != null && session.isConnected()) { + session.disconnect(); + } + } + + /** + * 关闭会话通道 + * + * @param channel 会话通道 + * @since 4.0.3 + */ + public static void close(final Channel channel) { + if (channel != null && channel.isConnected()) { + channel.disconnect(); + } + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjSession.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjSession.java index 99b6149f9..e738ddec3 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjSession.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjSession.java @@ -13,7 +13,6 @@ package org.dromara.hutool.extra.ssh.engine.sshj; import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.transport.verification.PromiscuousVerifier; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.util.CharsetUtil; @@ -40,17 +39,21 @@ public class SshjSession implements Session { * @param connector {@link Connector},保存连接和验证信息等 */ public SshjSession(final Connector connector) { - final SSHClient ssh = new SSHClient(); - ssh.addHostKeyVerifier(new PromiscuousVerifier()); + this(SshjUtil.openClient(connector)); + } + + /** + * 构造 + * + * @param ssh {@link SSHClient} + */ + public SshjSession(final SSHClient ssh) { + this.ssh = ssh; try { - ssh.connect(connector.getHost(), connector.getPort()); - ssh.authPassword(connector.getUser(), connector.getPassword()); this.raw = ssh.startSession(); } catch (final IOException e) { throw new IORuntimeException(e); } - - this.ssh = ssh; } /** @@ -63,10 +66,19 @@ public class SshjSession implements Session { } @Override - public Object getRaw() { + public net.schmizz.sshj.connection.channel.direct.Session getRaw() { return raw; } + /** + * 是否连接状态 + * + * @return 是否连接状态 + */ + public boolean isConnected() { + return null != this.raw && (null == this.ssh || this.ssh.isConnected()); + } + @Override public void close() throws IOException { IoUtil.closeQuietly(this.raw); diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjUtil.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjUtil.java new file mode 100644 index 000000000..48a1a702d --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/ssh/engine/sshj/SshjUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.extra.ssh.engine.sshj; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; +import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.extra.ssh.Connector; + +import java.io.IOException; + +/** + * 基于SSHJ(https://github.com/hierynomus/sshj)相关工具类 + * + * @author looly + */ +public class SshjUtil { + + /** + * 打开客户端连接 + * + * @param connector 连接信息 + * @return {@link SSHClient} + */ + public static SSHClient openClient(final Connector connector) { + final SSHClient ssh = new SSHClient(); + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.setConnectTimeout((int) connector.getTimeout()); + ssh.setTimeout((int) connector.getTimeout()); + + try { + ssh.connect(connector.getHost(), connector.getPort()); + ssh.authPassword(connector.getUser(), connector.getPassword()); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + + return ssh; + } +} diff --git a/hutool-extra/src/test/java/org/dromara/hutool/extra/ftp/FtpTest.java b/hutool-extra/src/test/java/org/dromara/hutool/extra/ftp/FtpTest.java index 10d95be66..027e7d885 100644 --- a/hutool-extra/src/test/java/org/dromara/hutool/extra/ftp/FtpTest.java +++ b/hutool-extra/src/test/java/org/dromara/hutool/extra/ftp/FtpTest.java @@ -16,7 +16,7 @@ import org.apache.commons.net.ftp.FTPSClient; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.lang.Console; -import org.dromara.hutool.extra.ssh.engine.jsch.Sftp; +import org.dromara.hutool.extra.ssh.engine.jsch.JschSftp; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -94,7 +94,7 @@ public class FtpTest { @Test @Disabled public void recursiveDownloadFolderSftp() { - final Sftp ftp = new Sftp("127.0.0.1", 22, "test", "test"); + final JschSftp ftp = new JschSftp("127.0.0.1", 22, "test", "test"); ftp.cd("/file/aaa"); Console.log(ftp.pwd()); @@ -131,7 +131,7 @@ public class FtpTest { @Test @Disabled public void existSftpTest() { - try (final Sftp ftp = new Sftp("127.0.0.1", 22, "test", "test")) { + try (final JschSftp ftp = new JschSftp("127.0.0.1", 22, "test", "test")) { Console.log(ftp.pwd()); Console.log(ftp.exist(null)); Console.log(ftp.exist("")); diff --git a/hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschUtilTest.java b/hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschTest.java similarity index 58% rename from hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschUtilTest.java rename to hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschTest.java index 76556ba22..543fda058 100644 --- a/hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschUtilTest.java +++ b/hutool-extra/src/test/java/org/dromara/hutool/extra/ssh/JschTest.java @@ -14,9 +14,9 @@ package org.dromara.hutool.extra.ssh; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.lang.Console; -import com.jcraft.jsch.Session; -import org.dromara.hutool.extra.ssh.engine.jsch.JschUtil; -import org.dromara.hutool.extra.ssh.engine.jsch.Sftp; +import org.dromara.hutool.core.util.CharsetUtil; +import org.dromara.hutool.extra.ssh.engine.jsch.JschSession; +import org.dromara.hutool.extra.ssh.engine.jsch.JschSftp; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,25 +27,26 @@ import org.junit.jupiter.api.Test; * @author looly * */ -public class JschUtilTest { +public class JschTest { + @SuppressWarnings("resource") @Test @Disabled public void bindPortTest() { //新建会话,此会话用于ssh连接到跳板机(堡垒机),此处为10.1.1.1:22 - final Session session = JschUtil.getSession("looly.centos", 22, "test", "123456"); + final JschSession session = new JschSession(new Connector("looly.centos", 22, "test", "123456")); // 将堡垒机保护的内网8080端口映射到localhost,我们就可以通过访问http://localhost:8080/访问内网服务了 - JschUtil.bindPort(session, "172.20.12.123", 8080, 8080); + session.bindLocalPort("172.20.12.123", 8080, 8080); } - + @SuppressWarnings("resource") @Test @Disabled public void bindRemotePort() { // 建立会话 - final Session session = JschUtil.getSession("looly.centos", 22, "test", "123456"); + final JschSession session = new JschSession(new Connector("looly.centos", 22, "test", "123456")); // 绑定ssh服务端8089端口到本机的8000端口上 - final boolean b = JschUtil.bindRemotePort(session, 8089, "localhost", 8000); + final boolean b = session.bindRemotePort(8089, "localhost", 8000); Assertions.assertTrue(b); // 保证一直运行 } @@ -54,43 +55,38 @@ public class JschUtilTest { @Test @Disabled public void sftpTest() { - final Session session = JschUtil.getSession("looly.centos", 22, "root", "123456"); - final Sftp sftp = JschUtil.createSftp(session); - sftp.mkDirs("/opt/test/aaa/bbb"); + final JschSession session = new JschSession(new Connector("looly.centos", 22, "root", "123456")); + final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8); + jschSftp.mkDirs("/opt/test/aaa/bbb"); Console.log("OK"); } + @SuppressWarnings("CallToPrintStackTrace") @Test @Disabled public void reconnectIfTimeoutTest() throws InterruptedException { - final Session session = JschUtil.getSession("sunnyserver", 22,"mysftp","liuyang1234"); - final Sftp sftp = JschUtil.createSftp(session); + final JschSession session = new JschSession(new Connector("sunnyserver", 22,"mysftp","liuyang1234")); + final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8); - Console.log("打印pwd: " + sftp.pwd()); - Console.log("cd / : " + sftp.cd("/")); + Console.log("打印pwd: " + jschSftp.pwd()); + Console.log("cd / : " + jschSftp.cd("/")); Console.log("休眠一段时间,查看是否超时"); Thread.sleep(30 * 1000); try{ // 当连接超时时,isConnected()仍然返回true,pwd命令也能正常返回,因此,利用发送cd命令的返回结果,来判断是否连接超时 - Console.log("isConnected " + sftp.getClient().isConnected()); - Console.log("打印pwd: " + sftp.pwd()); - Console.log("cd / : " + sftp.cd("/")); + Console.log("isConnected " + jschSftp.getClient().isConnected()); + Console.log("打印pwd: " + jschSftp.pwd()); + Console.log("cd / : " + jschSftp.cd("/")); }catch (final SshException e) { e.printStackTrace(); } Console.log("调用reconnectIfTimeout方法,判断是否超时并重连"); - sftp.reconnectIfTimeout(); + jschSftp.reconnectIfTimeout(); - Console.log("打印pwd: " + sftp.pwd()); + Console.log("打印pwd: " + jschSftp.pwd()); - IoUtil.closeQuietly(sftp); - } - - @Test - @Disabled - public void getSessionTest(){ - JschUtil.getSession("192.168.1.134", 22, "root", "aaa", null); + IoUtil.closeQuietly(jschSftp); } }