|
@@ -0,0 +1,383 @@
|
|
|
+package com.nokia.common.ssh;
|
|
|
+
|
|
|
+import com.jcraft.jsch.*;
|
|
|
+import com.nokia.common.ssh.entity.SSHServer;
|
|
|
+import com.nokia.common.ssh.entity.UserInfoImpl;
|
|
|
+import com.nokia.common.ssh.exception.SSHUtilException;
|
|
|
+import com.nokia.common.ssh.exception.ScpAckErrorException;
|
|
|
+import com.xxl.job.core.context.XxlJobHelper;
|
|
|
+import lombok.Getter;
|
|
|
+import lombok.Setter;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+
|
|
|
+import java.io.*;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.NoSuchFileException;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Properties;
|
|
|
+import java.util.Vector;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+
|
|
|
+ * 使用jsch库实现的ssh的工具类
|
|
|
+ * <p>
|
|
|
+ * todo: scpTo和scpFrom 在本机和targetServer默认编码不一致的时候,文件名中的中文会乱码,但是不会影响到文件内容,
|
|
|
+ */
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+public class SSHUtil {
|
|
|
+
|
|
|
+ @Getter
|
|
|
+ @Setter
|
|
|
+ private SSHServer targetServer = new SSHServer();
|
|
|
+ private Session session = null;
|
|
|
+ private Channel channel = null;
|
|
|
+ private JSch jSch = null;
|
|
|
+ private FileInputStream fileInputStream = null;
|
|
|
+ private FileOutputStream fileOutputStream = null;
|
|
|
+ private OutputStream outputStream = null;
|
|
|
+ private InputStream inputStream = null;
|
|
|
+ private ChannelSftp channelSftp = null;
|
|
|
+
|
|
|
+ public SSHUtil() {
|
|
|
+ }
|
|
|
+
|
|
|
+ public SSHUtil(String host, String user, String password) {
|
|
|
+ targetServer = new SSHServer(host, 22, user, password);
|
|
|
+ }
|
|
|
+
|
|
|
+ public SSHUtil(String host, Integer port, String user, String password) {
|
|
|
+ targetServer = new SSHServer(host, port, user, password);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 获取文件列表
|
|
|
+ */
|
|
|
+ @SuppressWarnings("rawtypes")
|
|
|
+ public List<String> ls(String path) throws JSchException, SftpException {
|
|
|
+ getConnectSession();
|
|
|
+ channelSftpConnect();
|
|
|
+ List<String> fileNameList = new ArrayList<>();
|
|
|
+ Vector fileList = channelSftp.ls(path);
|
|
|
+ for (Object o : fileList) {
|
|
|
+ String fileName = ((ChannelSftp.LsEntry) o).getFilename();
|
|
|
+ if (".".equals(fileName) || "..".equals(fileName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ fileNameList.add(fileName);
|
|
|
+ }
|
|
|
+
|
|
|
+ return fileNameList.stream().sorted().collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 下载文件
|
|
|
+ */
|
|
|
+ public void get(String src, String dst) throws JSchException, SftpException, IOException {
|
|
|
+ try (OutputStream out = Files.newOutputStream(Paths.get(dst))) {
|
|
|
+ getConnectSession();
|
|
|
+ channelSftpConnect();
|
|
|
+ channelSftp.get(src, out);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 删除文件
|
|
|
+ */
|
|
|
+ public void rm(String path) throws JSchException, SftpException {
|
|
|
+ getConnectSession();
|
|
|
+ channelSftpConnect();
|
|
|
+ channelSftp.rm(path);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 远程执行指令
|
|
|
+ */
|
|
|
+ public String exec(String command) throws JSchException, IOException {
|
|
|
+ StringBuilder stringBuilder = new StringBuilder();
|
|
|
+ getConnectSession();
|
|
|
+ channel = session.openChannel("exec");
|
|
|
+
|
|
|
+ String execCommand;
|
|
|
+
|
|
|
+
|
|
|
+ if (command.startsWith("bash --login -c")) {
|
|
|
+ execCommand = command;
|
|
|
+ } else {
|
|
|
+ execCommand = String.format("bash --login -c \"%s\"", command);
|
|
|
+ }
|
|
|
+ ((ChannelExec) channel).setCommand(execCommand);
|
|
|
+ channel.setInputStream(null);
|
|
|
+ ((ChannelExec) channel).setErrStream(System.err);
|
|
|
+ InputStream in = channel.getInputStream();
|
|
|
+ channel.connect();
|
|
|
+ byte[] tmp = new byte[1024];
|
|
|
+ while (true) {
|
|
|
+ while (in.available() > 0) {
|
|
|
+ int i = in.read(tmp, 0, 1024);
|
|
|
+ if (i < 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ stringBuilder.append(new String(tmp, 0, i));
|
|
|
+ }
|
|
|
+ if (channel.isClosed()) {
|
|
|
+ if (in.available() > 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return stringBuilder.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 使用SCP把本地文件推送到targetServer目录下
|
|
|
+ * <p>
|
|
|
+ * 注意,文件名不能包含中文
|
|
|
+ */
|
|
|
+ public boolean scpTo(String sourceFilePath, String targetPath) throws JSchException, IOException, SSHUtilException {
|
|
|
+ getConnectSession();
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ String command = "scp " + "-t " + targetPath;
|
|
|
+ channel = session.openChannel("exec");
|
|
|
+ ((ChannelExec) channel).setCommand(command);
|
|
|
+ outputStream = channel.getOutputStream();
|
|
|
+ inputStream = channel.getInputStream();
|
|
|
+ channel.connect();
|
|
|
+ if (checkAck(inputStream) != 0) {
|
|
|
+ log.error("scpTo 执行失败");
|
|
|
+ XxlJobHelper.log("scpTo 执行失败");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ File sourceFile = new File(sourceFilePath);
|
|
|
+ if (sourceFile.isDirectory()) {
|
|
|
+ log.error("sourceFilePath 必须是文件");
|
|
|
+ XxlJobHelper.log("sourceFilePath 必须是文件");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ long fileSize = sourceFile.length();
|
|
|
+ command = "C0644 " + fileSize + " " + sourceFile.getName() + "\n";
|
|
|
+ outputStream.write(command.getBytes());
|
|
|
+ outputStream.flush();
|
|
|
+ if (checkAck(inputStream) != 0) {
|
|
|
+ log.error("scpTo 执行失败");
|
|
|
+ XxlJobHelper.log("scpTo 执行失败");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ fileInputStream = new FileInputStream(sourceFile);
|
|
|
+ byte[] buffer = new byte[1024];
|
|
|
+ while (true) {
|
|
|
+ int len = fileInputStream.read(buffer, 0, buffer.length);
|
|
|
+ if (len <= 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ outputStream.write(buffer, 0, len);
|
|
|
+ }
|
|
|
+ buffer[0] = 0;
|
|
|
+ outputStream.write(buffer, 0, 1);
|
|
|
+ outputStream.flush();
|
|
|
+ return checkAck(inputStream) == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 使用scp把targetServer目录下的文件复制到本地
|
|
|
+ */
|
|
|
+ public boolean scpFrom(String sourceFilePath, String targetPath) throws JSchException, IOException, SSHUtilException {
|
|
|
+ log.info(sourceFilePath);
|
|
|
+ XxlJobHelper.log(sourceFilePath);
|
|
|
+ getConnectSession();
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ String command = "scp -f " + sourceFilePath;
|
|
|
+ Channel channel = session.openChannel("exec");
|
|
|
+ ((ChannelExec) channel).setCommand(command);
|
|
|
+ outputStream = channel.getOutputStream();
|
|
|
+ inputStream = channel.getInputStream();
|
|
|
+ channel.connect();
|
|
|
+ byte[] buf = new byte[1024];
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ buf[0] = 0;
|
|
|
+ outputStream.write(buf, 0, 1);
|
|
|
+ outputStream.flush();
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ int c = checkAck(inputStream);
|
|
|
+
|
|
|
+ if (c == 'C') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ inputStream.read(buf, 0, 5);
|
|
|
+
|
|
|
+ long filesize = 0L;
|
|
|
+ while (true) {
|
|
|
+ if (inputStream.read(buf, 0, 1) < 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (buf[0] == ' ') {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ filesize = filesize * 10L + (long) (buf[0] - '0');
|
|
|
+ }
|
|
|
+
|
|
|
+ String file = null;
|
|
|
+ for (int i = 0; ; i++) {
|
|
|
+ inputStream.read(buf, i, 1);
|
|
|
+
|
|
|
+ if (buf[i] == (byte) 0x0a) {
|
|
|
+ file = new String(buf, 0, i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("filesize={}, file={}", filesize, file);
|
|
|
+ XxlJobHelper.log("filesize={}, file={}", filesize, file);
|
|
|
+
|
|
|
+ buf[0] = 0;
|
|
|
+ outputStream.write(buf, 0, 1);
|
|
|
+ outputStream.flush();
|
|
|
+
|
|
|
+ File target = new File(targetPath);
|
|
|
+ if (target.isDirectory()) {
|
|
|
+ log.info("{} 是目录,需要添加文件名", target.getAbsolutePath());
|
|
|
+ XxlJobHelper.log("{} 是目录,需要添加文件名", target.getAbsolutePath());
|
|
|
+ target = new File(targetPath + File.separator + file);
|
|
|
+ }
|
|
|
+
|
|
|
+ fileOutputStream = new FileOutputStream(target);
|
|
|
+ int foo;
|
|
|
+ while (true) {
|
|
|
+ if (buf.length < filesize) {
|
|
|
+ foo = buf.length;
|
|
|
+ } else {
|
|
|
+ foo = (int) filesize;
|
|
|
+ }
|
|
|
+ foo = inputStream.read(buf, 0, foo);
|
|
|
+ if (foo < 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ fileOutputStream.write(buf, 0, foo);
|
|
|
+ filesize -= foo;
|
|
|
+ if (filesize == 0L) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (checkAck(inputStream) != 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ buf[0] = 0;
|
|
|
+ outputStream.write(buf, 0, 1);
|
|
|
+ outputStream.flush();
|
|
|
+ log.info("scp from {}@{}:{}{} to {} 完成", targetServer.getUser(), targetServer.getHost(), targetServer.getPort(), sourceFilePath, target.getAbsolutePath());
|
|
|
+ XxlJobHelper.log("scp from {}@{}:{}{} to {} 完成", targetServer.getUser(), targetServer.getHost(), targetServer.getPort(), sourceFilePath, target.getAbsolutePath());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void getConnectSession() throws JSchException {
|
|
|
+ if (jSch == null) {
|
|
|
+ jSch = new JSch();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (session == null) {
|
|
|
+ session = jSch.getSession(targetServer.getUser(), targetServer.getHost(), targetServer.getPort());
|
|
|
+ session.setPassword(targetServer.getPassword());
|
|
|
+ session.setUserInfo(new UserInfoImpl());
|
|
|
+
|
|
|
+ Properties properties = new Properties();
|
|
|
+ properties.put("StrictHostKeyChecking", "no");
|
|
|
+ session.setConfig(properties);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!session.isConnected()) {
|
|
|
+ session.connect();
|
|
|
+ log.info("已连接到{}@{}:{}", targetServer.getUser(), targetServer.getHost(), targetServer.getPort());
|
|
|
+ XxlJobHelper.log("已连接到{}@{}:{}", targetServer.getUser(), targetServer.getHost(), targetServer.getPort());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void channelSftpConnect() throws JSchException {
|
|
|
+ if (channelSftp == null) {
|
|
|
+ channelSftp = (ChannelSftp) session.openChannel("sftp");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!channelSftp.isConnected()) {
|
|
|
+ channelSftp.connect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void disconnect() throws IOException {
|
|
|
+ if (fileOutputStream != null) {
|
|
|
+ fileOutputStream.close();
|
|
|
+ fileOutputStream = null;
|
|
|
+ }
|
|
|
+ if (fileInputStream != null) {
|
|
|
+ fileInputStream.close();
|
|
|
+ fileInputStream = null;
|
|
|
+ }
|
|
|
+ if (outputStream != null) {
|
|
|
+ outputStream.close();
|
|
|
+ outputStream = null;
|
|
|
+ }
|
|
|
+ if (channel != null) {
|
|
|
+ channel.disconnect();
|
|
|
+ channel = null;
|
|
|
+ }
|
|
|
+ if (channelSftp != null) {
|
|
|
+ channelSftp.quit();
|
|
|
+ }
|
|
|
+ if (session != null) {
|
|
|
+ session.disconnect();
|
|
|
+ session = null;
|
|
|
+ }
|
|
|
+ jSch = null;
|
|
|
+ log.info("jsch disconnected");
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 来自源端的每条消息和每个传输完毕的文件都需要宿端的确认和响应.
|
|
|
+ * 宿端会返回三种确认消息: 0(正常), 1(警告)或2(严重错误, 将中断连接).
|
|
|
+ * 消息1和2可以跟一个字符串和一个换行符, 这个字符串将显示在scp的源端. 无论这个字符串是否为空, 换行符都是不可缺少的.
|
|
|
+ */
|
|
|
+ private static int checkAck(InputStream in) throws IOException, SSHUtilException {
|
|
|
+ int b = in.read();
|
|
|
+
|
|
|
+ if (b == 0) {
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+ if (b == -1) {
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (b == 1 || b == 2) {
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ int c;
|
|
|
+ do {
|
|
|
+
|
|
|
+ c = in.read();
|
|
|
+ sb.append((char) c);
|
|
|
+ } while (c != '\n');
|
|
|
+ log.info("checkAck发现错误消息: ack={}-msg={}", b, sb);
|
|
|
+ XxlJobHelper.log("checkAck发现错误消息: ack={}-msg={}", b, sb);
|
|
|
+ if (b == 1 && sb.toString().endsWith("No such file or directory")) {
|
|
|
+ throw new NoSuchFileException(sb.toString());
|
|
|
+ } else {
|
|
|
+ throw new ScpAckErrorException(sb.toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+}
|