This commit is contained in:
Looly 2023-09-24 17:16:21 +08:00
parent ed0b9a16ef
commit 9461336639
10 changed files with 568 additions and 244 deletions

View File

@ -23,6 +23,7 @@ public class Connector {
private String user = "root"; private String user = "root";
private String password; private String password;
private String group; private String group;
private long timeout;
/** /**
* 构造 * 构造
@ -52,10 +53,24 @@ public class Connector {
* @param password 密码 * @param password 密码
*/ */
public Connector(final String host, final int port, final String user, final String 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.host = host;
this.port = port; this.port = port;
this.user = user; this.user = user;
this.password = password; this.password = password;
this.timeout = timeout;
} }
/** /**
@ -148,11 +163,36 @@ public class Connector {
this.group = group; this.group = group;
} }
/**
* 获得连接超时时间
*
* @return 连接超时时间
*/
public long getTimeout() {
return timeout;
}
/**
* 设置连接超时时间
*
* @param timeout 连接超时时间
*/
public void setTimeout(final long timeout) {
this.timeout = timeout;
}
/** /**
* toString方法仅用于测试显示 * toString方法仅用于测试显示
*/ */
@Override @Override
public String toString() { 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 +
'}';
} }
} }

View File

@ -13,16 +13,22 @@
package org.dromara.hutool.extra.ssh.engine.ganymed; package org.dromara.hutool.extra.ssh.engine.ganymed;
import ch.ethz.ssh2.Connection; import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.LocalPortForwarder;
import ch.ethz.ssh2.StreamGobbler; import ch.ethz.ssh2.StreamGobbler;
import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil; 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.core.util.CharsetUtil;
import org.dromara.hutool.extra.ssh.Connector; import org.dromara.hutool.extra.ssh.Connector;
import org.dromara.hutool.extra.ssh.Session; import org.dromara.hutool.extra.ssh.Session;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
/** /**
* {@link ch.ethz.ssh2.Session}包装 * {@link ch.ethz.ssh2.Session}包装
@ -31,15 +37,28 @@ import java.nio.charset.Charset;
*/ */
public class GanymedSession implements Session { public class GanymedSession implements Session {
private Connection connection;
private final ch.ethz.ssh2.Session raw; private final ch.ethz.ssh2.Session raw;
private Map<Integer, LocalPortForwarder> localPortForwarderMap;
/** /**
* 构造 * 构造
* *
* @param connector {@link Connector}保存连接和验证信息等 * @param connector {@link Connector}保存连接和验证信息等
*/ */
public GanymedSession(final Connector 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} * @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; this.raw = raw;
} }
@ -61,6 +80,104 @@ public class GanymedSession implements Session {
if (raw != null) { if (raw != null) {
raw.close(); 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端口上. <br>
* 即数据从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); 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); 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);
}
}
} }

View File

@ -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);
}
}
}

View File

@ -15,6 +15,7 @@ package org.dromara.hutool.extra.ssh.engine.jsch;
import com.jcraft.jsch.*; import com.jcraft.jsch.*;
import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil; 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.ByteUtil;
import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.extra.ssh.Connector; import org.dromara.hutool.extra.ssh.Connector;
@ -32,6 +33,7 @@ import java.nio.charset.Charset;
public class JschSession implements Session { public class JschSession implements Session {
private final com.jcraft.jsch.Session raw; private final com.jcraft.jsch.Session raw;
private final long timeout;
/** /**
* 构造 * 构造
@ -39,28 +41,51 @@ public class JschSession implements Session {
* @param connector {@link Connector}保存连接和验证信息等 * @param connector {@link Connector}保存连接和验证信息等
*/ */
public JschSession(final Connector 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.raw = raw;
this.timeout = timeout;
} }
@Override @Override
public Object getRaw() { public com.jcraft.jsch.Session getRaw() {
return raw; return this.raw;
}
/**
* 是否连接状态
*
* @return 是否连接状态
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isConnected() {
return null != this.raw && this.raw.isConnected();
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException {
if (raw != null && raw.isConnected()) { JschUtil.close(this.raw);
raw.disconnect();
} }
/**
* 绑定端口到本地 一个会话可绑定多个端口
*
* @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,8 +98,11 @@ public class JschSession implements Session {
* @return 成功与否 * @return 成功与否
* @throws SshException 端口绑定失败异常 * @throws SshException 端口绑定失败异常
*/ */
public boolean bindPort(final String remoteHost, final int remotePort, final String localHost, final int localPort) throws SshException { public boolean bindLocalPort(final String remoteHost, final int remotePort, final String localHost, final int localPort) throws SshException {
if (this.raw != null && this.raw.isConnected()) { if (!isConnected()) {
return false;
}
try { try {
this.raw.setPortForwardingL(localHost, localPort, remoteHost, remotePort); this.raw.setPortForwardingL(localHost, localPort, remoteHost, remotePort);
} catch (final JSchException e) { } catch (final JSchException e) {
@ -82,7 +110,18 @@ public class JschSession implements Session {
} }
return true; return true;
} }
return false;
/**
* 解除远程端口映射
*
* @param localPort 需要解除的本地端口
*/
public void unBindLocalPort(final int localPort) {
try {
this.raw.delPortForwardingL(localPort);
} catch (final JSchException e) {
throw new SshException(e);
}
} }
/** /**
@ -96,7 +135,10 @@ public class JschSession implements Session {
* @throws SshException 端口绑定失败异常 * @throws SshException 端口绑定失败异常
*/ */
public boolean bindRemotePort(final int bindPort, final String host, final int port) throws SshException { public boolean bindRemotePort(final int bindPort, final String host, final int port) throws SshException {
if (this.raw != null && this.raw.isConnected()) { if (!isConnected()) {
return false;
}
try { try {
this.raw.setPortForwardingR(bindPort, host, port); this.raw.setPortForwardingR(bindPort, host, port);
} catch (final JSchException e) { } catch (final JSchException e) {
@ -104,17 +146,15 @@ public class JschSession implements Session {
} }
return true; return true;
} }
return false;
}
/** /**
* 解除端口映射 * 解除远程端口映射
* *
* @param localPort 需要解除的本地端口 * @param localPort 需要解除的本地端口
*/ */
public void unBindPort(final int localPort) { public void unBindRemotePort(final int localPort) {
try { try {
this.raw.delPortForwardingL(localPort); this.raw.delPortForwardingR(localPort);
} catch (final JSchException e) { } catch (final JSchException e) {
throw new SshException(e); throw new SshException(e);
} }
@ -127,16 +167,7 @@ public class JschSession implements Session {
* @return {@link Channel} * @return {@link Channel}
*/ */
public Channel createChannel(final ChannelType channelType) { public Channel createChannel(final ChannelType channelType) {
final Channel channel; return JschUtil.createChannel(this.raw, channelType, this.timeout);
try {
if (!this.raw.isConnected()) {
this.raw.connect();
}
channel = this.raw.openChannel(channelType.getValue());
} catch (final JSchException e) {
throw new SshException(e);
}
return channel;
} }
/** /**
@ -155,24 +186,17 @@ public class JschSession implements Session {
* @return {@link Channel} * @return {@link Channel}
*/ */
public Channel openChannel(final ChannelType channelType) { 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 charset 编码
* @param timeout 连接超时时长单位毫秒 * @return {@link JschSftp}
* @return {@link Channel}
*/ */
public Channel openChannel(final ChannelType channelType, final int timeout) { public JschSftp openSftp(final Charset charset) {
final Channel channel = createChannel(channelType); return new JschSftp(this.raw, charset, this.timeout);
try {
channel.connect(Math.max(timeout, 0));
} catch (final JSchException e) {
throw new SshException(e);
}
return channel;
} }
/** /**
@ -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;
}
} }

View File

@ -12,6 +12,7 @@
package org.dromara.hutool.extra.ssh.engine.jsch; 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.CollUtil;
import org.dromara.hutool.core.collection.ListUtil; import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.io.file.FileUtil; 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.AbstractFtp;
import org.dromara.hutool.extra.ftp.FtpConfig; import org.dromara.hutool.extra.ftp.FtpConfig;
import org.dromara.hutool.extra.ftp.FtpException; import org.dromara.hutool.extra.ftp.FtpException;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry; import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.ChannelSftp.LsEntrySelector; import com.jcraft.jsch.ChannelSftp.LsEntrySelector;
import com.jcraft.jsch.Session; import org.dromara.hutool.extra.ssh.Connector;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import org.dromara.hutool.extra.ssh.SshException; import org.dromara.hutool.extra.ssh.SshException;
import java.io.File; import java.io.File;
@ -50,7 +47,7 @@ import java.util.function.Predicate;
* @author looly * @author looly
* @since 4.0.2 * @since 4.0.2
*/ */
public class Sftp extends AbstractFtp { public class JschSftp extends AbstractFtp {
private Session session; private Session session;
private ChannelSftp channel; private ChannelSftp channel;
@ -65,7 +62,7 @@ public class Sftp extends AbstractFtp {
* @param sshUser 远程主机用户名 * @param sshUser 远程主机用户名
* @param sshPass 远程主机密码 * @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); this(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);
} }
@ -79,7 +76,7 @@ public class Sftp extends AbstractFtp {
* @param charset 编码 * @param charset 编码
* @since 4.1.14 * @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)); this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));
} }
@ -89,7 +86,7 @@ public class Sftp extends AbstractFtp {
* @param config FTP配置 * @param config FTP配置
* @since 5.3.3 * @since 5.3.3
*/ */
public Sftp(final FtpConfig config) { public JschSftp(final FtpConfig config) {
this(config, true); this(config, true);
} }
@ -100,45 +97,13 @@ public class Sftp extends AbstractFtp {
* @param init 是否立即初始化 * @param init 是否立即初始化
* @since 5.8.4 * @since 5.8.4
*/ */
public Sftp(final FtpConfig config, final boolean init) { public JschSftp(final FtpConfig config, final boolean init) {
super(config); super(config);
if (init) { 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 超时时间单位毫秒 * @param timeOut 超时时间单位毫秒
* @since 5.8.4 * @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)); 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 超时时间单位毫秒 * @param timeOut 超时时间单位毫秒
* @since 5.8.4 * @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)); super(FtpConfig.of().setCharset(charset).setConnectionTimeout(timeOut));
init(channel, charset); this.channel = channel;
init();
} }
// ---------------------------------------------------------------------------------------- Constructor end // ---------------------------------------------------------------------------------------- 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() { 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();
} }
/** // 创建Channel
* 初始化
*
* @param config FTP配置
* @since 5.3.3
*/
public void init(final FtpConfig config) {
init(config.getHost(), config.getPort(), config.getUser(), config.getPassword(), config.getCharset());
}
/**
* 初始化
*
* @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 { try {
channel.setFilenameEncoding(charset.toString()); this.channel = (ChannelSftp) this.session.openChannel(ChannelType.SFTP.getValue());
} catch (final SftpException e) { } catch (final JSchException e) {
throw new SshException(e);
}
}
try {
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); throw new SshException(e);
} }
this.channel = channel;
} }
@Override @Override
public Sftp reconnectIfTimeout() { public JschSftp reconnectIfTimeout() {
if (StrUtil.isBlank(this.ftpConfig.getHost())) { if (StrUtil.isBlank(this.ftpConfig.getHost())) {
throw new FtpException("Host is blank!"); throw new FtpException("Host is blank!");
} }
@ -554,7 +496,7 @@ public class Sftp extends AbstractFtp {
* @param destPath 目标路径 * @param destPath 目标路径
* @return this * @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); return put(srcFilePath, destPath, Mode.OVERWRITE);
} }
@ -566,7 +508,7 @@ public class Sftp extends AbstractFtp {
* @param mode {@link Mode} 模式 * @param mode {@link Mode} 模式
* @return this * @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); return put(srcFilePath, destPath, null, mode);
} }
@ -580,7 +522,7 @@ public class Sftp extends AbstractFtp {
* @return this * @return this
* @since 4.6.5 * @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 { try {
getClient().put(srcFilePath, destPath, monitor, mode.ordinal()); getClient().put(srcFilePath, destPath, monitor, mode.ordinal());
} catch (final SftpException e) { } catch (final SftpException e) {
@ -599,7 +541,7 @@ public class Sftp extends AbstractFtp {
* @return this * @return this
* @since 5.7.16 * @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 { try {
getClient().put(srcStream, destPath, monitor, mode.ordinal()); getClient().put(srcStream, destPath, monitor, mode.ordinal());
} catch (final SftpException e) { } catch (final SftpException e) {
@ -662,7 +604,7 @@ public class Sftp extends AbstractFtp {
* @param dest 目标文件路径 * @param dest 目标文件路径
* @return this * @return this
*/ */
public Sftp get(final String src, final String dest) { public JschSftp get(final String src, final String dest) {
try { try {
getClient().get(src, dest); getClient().get(src, dest);
} catch (final SftpException e) { } catch (final SftpException e) {
@ -679,7 +621,7 @@ public class Sftp extends AbstractFtp {
* @return this * @return this
* @since 5.7.0 * @since 5.7.0
*/ */
public Sftp get(final String src, final OutputStream out) { public JschSftp get(final String src, final OutputStream out) {
try { try {
getClient().get(src, out); getClient().get(src, out);
} catch (final SftpException e) { } catch (final SftpException e) {

View File

@ -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工具类<br>
* Jsch是Java Secure Channel的缩写JSch是一个SSH2的纯Java实现<br>
* 它允许你连接到一个SSH服务器并且可以使用端口转发X11转发文件传输等<br>
*
* @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();
}
}
}

View File

@ -13,7 +13,6 @@
package org.dromara.hutool.extra.ssh.engine.sshj; package org.dromara.hutool.extra.ssh.engine.sshj;
import net.schmizz.sshj.SSHClient; 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.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.core.util.CharsetUtil;
@ -40,17 +39,21 @@ public class SshjSession implements Session {
* @param connector {@link Connector}保存连接和验证信息等 * @param connector {@link Connector}保存连接和验证信息等
*/ */
public SshjSession(final Connector connector) { public SshjSession(final Connector connector) {
final SSHClient ssh = new SSHClient(); this(SshjUtil.openClient(connector));
ssh.addHostKeyVerifier(new PromiscuousVerifier()); }
/**
* 构造
*
* @param ssh {@link SSHClient}
*/
public SshjSession(final SSHClient ssh) {
this.ssh = ssh;
try { try {
ssh.connect(connector.getHost(), connector.getPort());
ssh.authPassword(connector.getUser(), connector.getPassword());
this.raw = ssh.startSession(); this.raw = ssh.startSession();
} catch (final IOException e) { } catch (final IOException e) {
throw new IORuntimeException(e); throw new IORuntimeException(e);
} }
this.ssh = ssh;
} }
/** /**
@ -63,10 +66,19 @@ public class SshjSession implements Session {
} }
@Override @Override
public Object getRaw() { public net.schmizz.sshj.connection.channel.direct.Session getRaw() {
return raw; return raw;
} }
/**
* 是否连接状态
*
* @return 是否连接状态
*/
public boolean isConnected() {
return null != this.raw && (null == this.ssh || this.ssh.isConnected());
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
IoUtil.closeQuietly(this.raw); IoUtil.closeQuietly(this.raw);

View File

@ -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;
/**
* 基于SSHJhttps://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;
}
}

View File

@ -16,7 +16,7 @@ import org.apache.commons.net.ftp.FTPSClient;
import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.lang.Console; 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.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -94,7 +94,7 @@ public class FtpTest {
@Test @Test
@Disabled @Disabled
public void recursiveDownloadFolderSftp() { 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"); ftp.cd("/file/aaa");
Console.log(ftp.pwd()); Console.log(ftp.pwd());
@ -131,7 +131,7 @@ public class FtpTest {
@Test @Test
@Disabled @Disabled
public void existSftpTest() { 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.pwd());
Console.log(ftp.exist(null)); Console.log(ftp.exist(null));
Console.log(ftp.exist("")); Console.log(ftp.exist(""));

View File

@ -14,9 +14,9 @@ package org.dromara.hutool.extra.ssh;
import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.lang.Console; import org.dromara.hutool.core.lang.Console;
import com.jcraft.jsch.Session; import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.extra.ssh.engine.jsch.JschUtil; import org.dromara.hutool.extra.ssh.engine.jsch.JschSession;
import org.dromara.hutool.extra.ssh.engine.jsch.Sftp; import org.dromara.hutool.extra.ssh.engine.jsch.JschSftp;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -27,25 +27,26 @@ import org.junit.jupiter.api.Test;
* @author looly * @author looly
* *
*/ */
public class JschUtilTest { public class JschTest {
@SuppressWarnings("resource")
@Test @Test
@Disabled @Disabled
public void bindPortTest() { public void bindPortTest() {
//新建会话此会话用于ssh连接到跳板机堡垒机此处为10.1.1.1:22 //新建会话此会话用于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/访问内网服务了 // 将堡垒机保护的内网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 @Test
@Disabled @Disabled
public void bindRemotePort() { 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端口上 // 绑定ssh服务端8089端口到本机的8000端口上
final boolean b = JschUtil.bindRemotePort(session, 8089, "localhost", 8000); final boolean b = session.bindRemotePort(8089, "localhost", 8000);
Assertions.assertTrue(b); Assertions.assertTrue(b);
// 保证一直运行 // 保证一直运行
} }
@ -54,43 +55,38 @@ public class JschUtilTest {
@Test @Test
@Disabled @Disabled
public void sftpTest() { public void sftpTest() {
final Session session = JschUtil.getSession("looly.centos", 22, "root", "123456"); final JschSession session = new JschSession(new Connector("looly.centos", 22, "root", "123456"));
final Sftp sftp = JschUtil.createSftp(session); final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8);
sftp.mkDirs("/opt/test/aaa/bbb"); jschSftp.mkDirs("/opt/test/aaa/bbb");
Console.log("OK"); Console.log("OK");
} }
@SuppressWarnings("CallToPrintStackTrace")
@Test @Test
@Disabled @Disabled
public void reconnectIfTimeoutTest() throws InterruptedException { public void reconnectIfTimeoutTest() throws InterruptedException {
final Session session = JschUtil.getSession("sunnyserver", 22,"mysftp","liuyang1234"); final JschSession session = new JschSession(new Connector("sunnyserver", 22,"mysftp","liuyang1234"));
final Sftp sftp = JschUtil.createSftp(session); final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8);
Console.log("打印pwd: " + sftp.pwd()); Console.log("打印pwd: " + jschSftp.pwd());
Console.log("cd / : " + sftp.cd("/")); Console.log("cd / : " + jschSftp.cd("/"));
Console.log("休眠一段时间,查看是否超时"); Console.log("休眠一段时间,查看是否超时");
Thread.sleep(30 * 1000); Thread.sleep(30 * 1000);
try{ try{
// 当连接超时时isConnected()仍然返回truepwd命令也能正常返回因此利用发送cd命令的返回结果来判断是否连接超时 // 当连接超时时isConnected()仍然返回truepwd命令也能正常返回因此利用发送cd命令的返回结果来判断是否连接超时
Console.log("isConnected " + sftp.getClient().isConnected()); Console.log("isConnected " + jschSftp.getClient().isConnected());
Console.log("打印pwd: " + sftp.pwd()); Console.log("打印pwd: " + jschSftp.pwd());
Console.log("cd / : " + sftp.cd("/")); Console.log("cd / : " + jschSftp.cd("/"));
}catch (final SshException e) { }catch (final SshException e) {
e.printStackTrace(); e.printStackTrace();
} }
Console.log("调用reconnectIfTimeout方法判断是否超时并重连"); Console.log("调用reconnectIfTimeout方法判断是否超时并重连");
sftp.reconnectIfTimeout(); jschSftp.reconnectIfTimeout();
Console.log("打印pwd: " + sftp.pwd()); Console.log("打印pwd: " + jschSftp.pwd());
IoUtil.closeQuietly(sftp); IoUtil.closeQuietly(jschSftp);
}
@Test
@Disabled
public void getSessionTest(){
JschUtil.getSession("192.168.1.134", 22, "root", "aaa", null);
} }
} }