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 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 +
'}';
}
}

View File

@ -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<Integer, LocalPortForwarder> 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端口上. <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);
}
@ -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);
}
}
}

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

View File

@ -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) {

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

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.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(""));

View File

@ -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()仍然返回truepwd命令也能正常返回因此利用发送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);
}
}