/**
* Copyright 2016 benjobs
* <p>
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.opencron.server.service;
import com.jcraft.jsch.*;
import org.opencron.common.utils.CommonUtils;
import org.opencron.common.utils.DigestUtils;
import org.opencron.server.dao.QueryDao;
import org.opencron.server.domain.Terminal;
import org.opencron.server.domain.User;
import org.opencron.server.job.OpencronTools;
import org.opencron.server.tag.PageBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.crypto.BadPaddingException;
import javax.servlet.http.HttpSession;
import java.io.*;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static org.opencron.common.utils.CommonUtils.notEmpty;
import static org.opencron.server.job.OpencronTools.HTTP_SESSION_ID;
import static org.opencron.server.job.OpencronTools.SSH_SESSION_ID;
/**
* @author <a href="mailto:benjobs@qq.com">benjobs@qq.com</a>
* @name:CommonUtil
* @version: 1.0.0
* @company: org.opencron
* @description: webconsole核心类
* @date: 2016-05-25 10:03<br/><br/>
* <p>
* <b style="color:RED"></b><br/><br/>
* 你快乐吗?<br/>
* 风轻轻的问我<br/>
* 曾经快乐过<br/>
* 那时的湖面<br/>
* 她踏着轻舟泛过<br/><br/>
* <p>
* 你忧伤吗?<br/>
* 雨悄悄的问我<br/>
* 一直忧伤着<br/>
* 此时的四季<br/>
* 全是她的柳絮飘落<br/><br/>
* <p>
* 你心痛吗?<br/>
* 泪偷偷的问我<br/>
* 心痛如刀割<br/>
* 收到记忆的包裹<br/>
* 都是她冰清玉洁还不曾雕琢<br/><br/>
* <p>
* <hr style="color:RED"/>
*/
@Service
@Transactional
public class TerminalService {
private static Logger logger = LoggerFactory.getLogger(TerminalService.class);
@Autowired
private QueryDao queryDao;
public boolean exists(Long userId, String host) throws Exception {
Terminal terminal = queryDao.sqlUniqueQuery(Terminal.class, "SELECT * FROM T_TERMINAL WHERE userId=? AND host=?", userId, host);
return terminal != null;
}
public boolean merge(Terminal term) throws Exception {
Terminal dbTerm = queryDao.sqlUniqueQuery(Terminal.class, "SELECT * FROM T_TERMINAL WHERE ID=?", term.getId());
if (dbTerm != null) {
term.setId(dbTerm.getId());
}
try {
queryDao.merge(term);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Terminal.AuthStatus auth(Terminal terminal) {
JSch jSch = new JSch();
Session session = null;
try {
session = jSch.getSession(terminal.getUserName(), terminal.getHost(), terminal.getPort());
session.setPassword(terminal.getPassword());
session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("PreferredAuthentications", "publickey,keyboard-interactive,password");
session.connect(TerminalClient.SESSION_TIMEOUT);
return Terminal.AuthStatus.SUCCESS;
} catch (Exception e) {
if (e.getMessage().toLowerCase().contains("userauth fail")) {
return Terminal.AuthStatus.PUBLIC_KEY_FAIL;
} else if (e.getMessage().toLowerCase().contains("auth fail") || e.getMessage().toLowerCase().contains("auth cancel")) {
return Terminal.AuthStatus.AUTH_FAIL;
} else if (e.getMessage().toLowerCase().contains("unknownhostexception")) {
logger.info("[opencron]:error: DNS Lookup Failed ");
return Terminal.AuthStatus.HOST_FAIL;
} else if (e instanceof BadPaddingException) {//RSA解码错误..密码错误...
return Terminal.AuthStatus.AUTH_FAIL;
} else {
return Terminal.AuthStatus.GENERIC_FAIL;
}
} finally {
if (session != null) {
session.disconnect();
}
}
}
public PageBean<Terminal> getPageBeanByUser(PageBean pageBean, Long userId) {
String sql = "SELECT * FROM T_TERMINAL WHERE USERID = ? ORDER By ";
pageBean.verifyOrderBy("name", "name", "host", "port", "logintime");
sql += pageBean.getOrderBy() + " " + pageBean.getOrder();
return queryDao.getPageBySql(pageBean, Terminal.class, sql, userId);
}
public Terminal getById(Long id) {
return queryDao.get(Terminal.class, id);
}
public String delete(HttpSession session, Long id) {
Terminal term = getById(id);
if (term == null) {
return "error";
}
User user = OpencronTools.getUser(session);
if (!OpencronTools.isPermission(session) && !user.getUserId().equals(term.getUserId())) {
return "error";
}
queryDao.createSQLQuery("DELETE FROM T_TERMINAL WHERE id=?", term.getId()).executeUpdate();
return "true";
}
public void login(Terminal terminal) {
terminal = getById(terminal.getId());
terminal.setLogintime(new Date());
queryDao.merge(terminal);
}
public List<Terminal> getListByUser(User user) {
String sql = "SELECT * FROM T_TERMINAL WHERE USERID = ? ";
return queryDao.sqlQuery(Terminal.class, sql, user.getUserId());
}
public void theme(Terminal terminal, String theme) throws Exception {
terminal.setTheme(theme);
merge(terminal);
}
public static class TerminalClient {
private String clientId;//每次生成的唯一值token
private String httpSessionId;//打开该终端的SessionId
private WebSocketSession webSocketSession;
private JSch jSch;
private ChannelShell channelShell;
private Session session;
private Terminal terminal;
private InputStream inputStream;
private OutputStream outputStream;
private BufferedWriter writer;
//获取路径相关变量
private String pwd;
private boolean sendTempCmd = false;
private String sendTempCmdId;
//控制命令(exit)终端退出相关变量
private boolean sendEnter;
private boolean receiveEnd = false;
private StringBuffer sendBuffer = new StringBuffer();
//连接时长相关变量
public static final int SERVER_ALIVE_INTERVAL = 60 * 1000;
public static final int SESSION_TIMEOUT = 60000;
public static final int CHANNEL_TIMEOUT = 60000;
private boolean closed = false;
public TerminalClient(WebSocketSession webSocketSession, Terminal terminal) {
this.webSocketSession = webSocketSession;
this.terminal = terminal;
this.httpSessionId = (String) webSocketSession.getAttributes().get(HTTP_SESSION_ID);
;
this.clientId = (String) webSocketSession.getAttributes().get(SSH_SESSION_ID);
this.sendTempCmdId = this.clientId + this.httpSessionId;
this.jSch = new JSch();
}
public void openTerminal(final int cols, int rows, int width, int height) throws Exception {
this.session = jSch.getSession(terminal.getUserName(), terminal.getHost(), terminal.getPort());
this.session.setPassword(terminal.getPassword());
this.session.setConfig("StrictHostKeyChecking", "no");
this.session.setConfig("PreferredAuthentications", "publickey,keyboard-interactive,password");
this.session.setServerAliveInterval(SERVER_ALIVE_INTERVAL);
this.session.connect(SESSION_TIMEOUT);
this.channelShell = (ChannelShell) session.openChannel("shell");
this.channelShell.setPtyType("xterm", cols, rows, width, height);
this.inputStream = this.channelShell.getInputStream();
this.outputStream = this.channelShell.getOutputStream();
this.writer = new BufferedWriter(new OutputStreamWriter(this.outputStream, "UTF-8"));
this.channelShell.connect();
new Thread(new Runnable() {
@Override
public void run() {
byte[] buffer = new byte[1024 * 4];
StringBuilder builder = new StringBuilder();
try {
while (webSocketSession != null && webSocketSession.isOpen()) {
builder.setLength(0);
int size = inputStream.read(buffer);
if (size == -1) {
return;
}
for (int i = 0; i < size; i++) {
char chr = (char) (buffer[i] & 0xff);
builder.append(chr);
}
//取到linux远程机器输出的信息发送给前端
String message = builder.toString();
message = new String(message.getBytes(DigestUtils.getEncoding(message)), "UTF-8");
//获取pwd的结果输出,不能发送给前台
if (sendTempCmd) {
if (message.contains(sendTempCmdId)) {
if (pwd != null || message.contains("echo")) {
continue;
}
pwd = message.replace(sendTempCmdId, "").replaceAll("\r\n.*", "") + "/";
logger.info("[opencron] Sftp upload file target path:{}", pwd);
}
} else {
webSocketSession.sendMessage(new TextMessage(message));
if (sendEnter) {
String trimMessage = message.replaceAll("\r\n", "");
if (CommonUtils.isEmpty(trimMessage)) {
receiveEnd = false;
} else {
if (sendBuffer.toString().equals("exit") && trimMessage.equals("logout")) {
closed = true;
}
receiveEnd = true;
}
if (receiveEnd) {
sendBuffer.setLength(0);
}
}
}
}
} catch (Exception e) {
}
}
}).start();
}
/**
* 向ssh终端输入内容
*
* @param message
* @throws IOException
*/
public void write(String message) throws IOException {
if (message.equals("\r")) {
if (closed) {
disconnect();
} else {
sendEnter = true;
}
} else {
sendEnter = false;
sendBuffer.append(message);
}
if (writer != null) {
writer.write(message);
writer.flush();
}
}
public void upload(String src, String dst, long fileSize) throws Exception {
FileInputStream file = new FileInputStream(src);
ChannelSftp channelSftp = (ChannelSftp) this.session.openChannel("sftp");
channelSftp.connect(CHANNEL_TIMEOUT);
//当前路径下,获取用户终端的当前位置
if (dst.startsWith("./")) {
//不出绝招不行啊....很神奇
sendTempCmd = true;
write(String.format("echo %s$(pwd)\r", this.sendTempCmdId));
//等待获取返回的路径...
Thread.sleep(100);
sendTempCmd = false;
if (pwd == null) {
throw new RuntimeException("[opencron] Sftp upload file target path error!");
}
dst = dst.replaceFirst("\\./", pwd);
pwd = null;
} else if (dst.startsWith("~")) {
dst = dst.replaceAll("~/|~", "");
}
channelSftp.put(file, dst, new OpencronSftpMonitor(fileSize));
//exit
if (channelSftp != null) {
channelSftp.exit();
}
//disconnect
if (channelSftp != null) {
channelSftp.disconnect();
}
}
public void disconnect() throws IOException {
if (writer != null) {
writer.close();
writer = null;
}
if (session != null) {
session.disconnect();
session = null;
}
if (jSch != null) {
jSch = null;
}
closed = true;
}
public void resize(Integer cols, Integer rows, Integer width, Integer height) throws IOException {
channelShell.setPtySize(cols, rows, width, height);
}
public Terminal getTerminal() {
return terminal;
}
public void setTerminal(Terminal terminal) {
this.terminal = terminal;
}
public boolean isClosed() {
return closed;
}
public WebSocketSession getWebSocketSession() {
return webSocketSession;
}
public String getHttpSessionId() {
return httpSessionId;
}
public String getClientId() {
return clientId;
}
}
public static class TerminalContext implements Serializable {
//key-->token value--->Terminal
public static Map<String, Terminal> terminalContext = new ConcurrentHashMap<String, Terminal>(0);
public static Terminal get(String key) {
return terminalContext.get(key);
}
public static void put(String key, Terminal terminal) {
//该终端实例只能被的打开一次,之后就失效
terminalContext.put(key, terminal);
//保存打开的实例,用于复制终端实例
OpencronTools.CACHE.put(key, terminal);
}
public static Terminal remove(String key) {
return terminalContext.remove(key);
}
}
public static class TerminalSession implements Serializable {
//key--->WebSocketSession value--->TerminalClient
public static Map<WebSocketSession, TerminalClient> terminalSession = new ConcurrentHashMap<WebSocketSession, TerminalClient>(0);
public static TerminalClient get(WebSocketSession key) {
return terminalSession.get(key);
}
public static TerminalClient get(String key) {
for (Map.Entry<WebSocketSession, TerminalClient> entry : terminalSession.entrySet()) {
TerminalClient client = entry.getValue();
if (client.getClientId().equals(key)) {
return client;
}
}
return null;
}
public static void put(WebSocketSession key, TerminalClient terminalClient) {
terminalSession.put(key, terminalClient);
}
public static TerminalClient remove(WebSocketSession key) {
return terminalSession.remove(key);
}
public static boolean isOpened(Terminal terminal) {
for (Map.Entry<WebSocketSession, TerminalClient> entry : terminalSession.entrySet()) {
if (entry.getValue().getTerminal().equals(terminal)) {
return true;
}
}
return false;
}
public static List<TerminalClient> findClient(Serializable sessionId) throws IOException {
List<TerminalClient> terminalClients = new ArrayList<TerminalClient>(0);
if (notEmpty(terminalSession)) {
for (Map.Entry<WebSocketSession, TerminalClient> entry : terminalSession.entrySet()) {
TerminalClient terminalClient = entry.getValue();
if (terminalClient != null && terminalClient.getTerminal() != null) {
if (sessionId.equals(terminalClient.getHttpSessionId())) {
terminalClients.add(terminalClient);
}
}
}
}
return terminalClients;
}
public static WebSocketSession findSession(Terminal terminal) {
for (Map.Entry<WebSocketSession, TerminalClient> entry : terminalSession.entrySet()) {
TerminalClient client = entry.getValue();
if (client.getTerminal().equals(terminal)) {
return entry.getKey();
}
}
return null;
}
public static void exit(String httpSessionId) throws IOException {
if (notEmpty(terminalSession)) {
for (Map.Entry<WebSocketSession, TerminalClient> entry : terminalSession.entrySet()) {
TerminalClient terminalClient = entry.getValue();
if (terminalClient.getHttpSessionId().equals(httpSessionId)) {
terminalClient.disconnect();
terminalClient.getWebSocketSession().sendMessage(new TextMessage("Sorry! Session was invalidated, so opencron Terminal changed to closed. "));
terminalClient.getWebSocketSession().close();
}
}
}
}
}
public static class OpencronSftpMonitor extends TimerTask implements SftpProgressMonitor {
private long progressInterval = 2 * 1000; // 默认间隔时间为5秒
private boolean isEnd = false; // 记录传输是否结束
private long transfered; // 记录已传输的数据总大小
private long fileSize; // 记录文件总大小
private Timer timer; // 定时器对象
private boolean isScheduled = false; // 记录是否已启动timer记时器
public OpencronSftpMonitor(long fileSize) {
this.fileSize = fileSize;
}
@Override
public void run() {
if (!isEnd()) { // 判断传输是否已结束
long transfered = getTransfered();
if (transfered != fileSize) { // 判断当前已传输数据大小是否等于文件总大小
sendProgressMessage(transfered);
} else {
logger.info("[opencron] Sftp file transfering is done.");
setEnd(true); // 如果当前已传输数据大小等于文件总大小,说明已完成,设置end
}
} else {
logger.info("[opencron] Sftp file transfering is done.cancel timer");
stop(); // 如果传输结束,停止timer记时器
return;
}
}
public void stop() {
logger.info("[opencron] Sftp progress monitor Stopping...");
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
isScheduled = false;
}
logger.info("[opencron] Sftp progress monitor Stoped.");
}
public void start() {
logger.info("[opencron] Sftp progress monitor Starting...");
if (timer == null) {
timer = new Timer();
}
timer.schedule(this, 1000, progressInterval);
isScheduled = true;
}
private void sendProgressMessage(long transfered) {
if (fileSize != 0) {
double d = ((double) transfered * 100) / (double) fileSize;
DecimalFormat df = new DecimalFormat("#.##");
logger.info("[opencron] Sftp Sending progress message: {} %", df.format(d));
} else {
logger.info("[opencron] Sftp Sending progress message: ", transfered);
}
}
public boolean count(long count) {
if (isEnd()) return false;
if (!isScheduled) {
start();
}
add(count);
return true;
}
public void end() {
setEnd(true);
}
private synchronized void add(long count) {
transfered = transfered + count;
}
private synchronized long getTransfered() {
return transfered;
}
public synchronized void setTransfered(long transfered) {
this.transfered = transfered;
}
private synchronized void setEnd(boolean isEnd) {
this.isEnd = isEnd;
}
private synchronized boolean isEnd() {
return isEnd;
}
public void init(int op, String src, String dest, long max) {
}
}
}