All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dromara.jpom.socket.handler.SshHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2019 Of Him Code Technology Studio
 * Jpom 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:
 * 			http://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.jpom.socket.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.NioUtil;
import cn.hutool.core.map.SafeConcurrentHashMap;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.extra.ssh.ChannelType;
import cn.hutool.extra.ssh.JschUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONValidator;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.Chars;
import org.bouncycastle.asn1.esf.SPuri;
import org.dromara.jpom.common.i18n.I18nMessageUtil;
import org.dromara.jpom.common.i18n.I18nThreadUtil;
import org.dromara.jpom.func.assets.model.MachineSshModel;
import org.dromara.jpom.model.data.SshModel;
import org.dromara.jpom.model.user.UserModel;
import org.dromara.jpom.permission.ClassFeature;
import org.dromara.jpom.permission.Feature;
import org.dromara.jpom.permission.MethodFeature;
import org.dromara.jpom.service.dblog.SshTerminalExecuteLogService;
import org.dromara.jpom.service.node.ssh.SshService;
import org.dromara.jpom.service.user.UserBindWorkspaceService;
import org.dromara.jpom.util.SocketSessionUtil;
import org.dromara.jpom.util.StringUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * ssh 处理2
 *
 * @author bwcx_jzy
 * @since 2019/8/9
 */
@Feature(cls = ClassFeature.SSH_TERMINAL, method = MethodFeature.EXECUTE)
@Slf4j
public class SshHandler extends BaseTerminalHandler {

    private static final ConcurrentHashMap HANDLER_ITEM_CONCURRENT_HASH_MAP = new SafeConcurrentHashMap<>();
    private static SshTerminalExecuteLogService sshTerminalExecuteLogService;
    private static UserBindWorkspaceService userBindWorkspaceService;
    private static SshService sshService;

    private static void init() {
        if (sshTerminalExecuteLogService == null) {
            sshTerminalExecuteLogService = SpringUtil.getBean(SshTerminalExecuteLogService.class);
        }
        if (userBindWorkspaceService == null) {
            userBindWorkspaceService = SpringUtil.getBean(UserBindWorkspaceService.class);
        }
        if (sshService == null) {
            sshService = SpringUtil.getBean(SshService.class);
        }
    }

    @Override
    public void afterConnectionEstablishedImpl(WebSocketSession session) throws Exception {
        super.afterConnectionEstablishedImpl(session);
        init();
        Map attributes = session.getAttributes();
        MachineSshModel machineSshModel = (MachineSshModel) attributes.get("machineSsh");
        SshModel sshModel = (SshModel) attributes.get("dataItem");
        //
        UserModel userInfo = (UserModel) attributes.get("userInfo");
        if (sshModel != null) {
            // 判断是没有任何限制
            String workspaceId = sshModel.getWorkspaceId();
            boolean sshCommandNotLimited = userBindWorkspaceService.exists(userInfo, workspaceId + UserBindWorkspaceService.SSH_COMMAND_NOT_LIMITED);
            attributes.put("sshCommandNotLimited", sshCommandNotLimited);
        } else {
            // 通过资产管理方式进入
            attributes.put("sshCommandNotLimited", true);
        }
        //
        HandlerItem handlerItem;
        try {
            //
            handlerItem = new HandlerItem(session, machineSshModel, sshModel);
            handlerItem.startRead();
        } catch (Exception e) {
            // 输出超时日志 @author jzy
            log.error(I18nMessageUtil.get("i18n.ssh_console_connection_timeout.8eb3"), e);
            sendBinary(session, I18nMessageUtil.get("i18n.ssh_console_connection_timeout.8eb3"));
            this.destroy(session);
            return;
        }
        HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);
        //
        Thread.sleep(1000);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
        if (handlerItem == null) {
            sendBinary(session, I18nMessageUtil.get("i18n.already_offline.d3b5"));
            IoUtil.close(session);
            return;
        }
        String payload = message.getPayload();

        JSONValidator.Type type = StringUtil.validatorJson(payload);
        if (type == JSONValidator.Type.Object) {
            JSONObject jsonObject = JSONObject.parseObject(payload);
            String data = jsonObject.getString("data");
            if (StrUtil.equals(data, "jpom-heart")) {
                // 心跳消息不转发
                return;
            }
            if (StrUtil.equals(data, "resize")) {
                // 缓存区大小
                handlerItem.resize(jsonObject);
                return;
            }
        }
        //
        Map attributes = session.getAttributes();
        UserModel userInfo = (UserModel) attributes.get("userInfo");
        boolean sshCommandNotLimited = (boolean) attributes.get("sshCommandNotLimited");
        try {
            this.sendCommand(handlerItem, payload, userInfo, sshCommandNotLimited);
        } catch (Exception e) {
            sendBinary(session, "Failure:" + e.getMessage());
            log.error(I18nMessageUtil.get("i18n.command_execution_exception.4ccd"), e);
        }
    }

    private void sendCommand(HandlerItem handlerItem, String data, UserModel userInfo, boolean sshCommandNotLimited) throws Exception {
        if (handlerItem.checkInput(data, userInfo, sshCommandNotLimited)) {
            handlerItem.outputStream.write(data.getBytes());
        } else {
            handlerItem.outputStream.write(I18nMessageUtil.get("i18n.no_permission_to_execute_command.04d4").getBytes());
            handlerItem.outputStream.flush();
            handlerItem.outputStream.write(new byte[]{3});
        }
        handlerItem.outputStream.flush();
    }

    /**
     * 记录终端执行记录
     *
     * @param session 回话
     * @param command 命令行
     * @param refuse  是否拒绝
     */
    private void logCommands(WebSocketSession session, String command, boolean refuse) {
        List split = StrUtil.splitTrim(command, StrUtil.CR);
        // 最后一个是否为回车, 最后一个不是回车表示还未提交,还在缓存去待确认
        boolean all = StrUtil.endWith(command, StrUtil.CR);
        int size = split.size();
        split = CollUtil.sub(split, 0, all ? size : size - 1);
        if (CollUtil.isEmpty(split)) {
            return;
        }
        // 获取基础信息
        Map attributes = session.getAttributes();
        UserModel userInfo = (UserModel) attributes.get("userInfo");
        String ip = (String) attributes.get("ip");
        String userAgent = (String) attributes.get(HttpHeaders.USER_AGENT);
        MachineSshModel machineSshModel = (MachineSshModel) attributes.get("machineSsh");
        SshModel sshItem = (SshModel) attributes.get("dataItem");
        //
        sshTerminalExecuteLogService.batch(userInfo, machineSshModel, sshItem, ip, userAgent, refuse, split);
    }

    private class HandlerItem implements Runnable, AutoCloseable {
        private final WebSocketSession session;
        private final InputStream inputStream;
        private final OutputStream outputStream;
        private final Session openSession;
        private final ChannelShell channel;
        private final SshModel sshItem;
        private final MachineSshModel machineSshModel;
        private final StringBuilder nowLineInput = new StringBuilder();
        private final KeyEventCycle keyEventCycle = new KeyEventCycle();

        HandlerItem(WebSocketSession session, MachineSshModel machineSshModel, SshModel sshModel) throws IOException {
            this.session = session;
            this.sshItem = sshModel;
            this.machineSshModel = machineSshModel;
            this.openSession = sshService.getSessionByModel(machineSshModel);
            this.channel = (ChannelShell) JschUtil.createChannel(openSession, ChannelType.SHELL);
            this.inputStream = channel.getInputStream();
            this.outputStream = channel.getOutputStream();
            keyEventCycle.setCharset(machineSshModel.charset());
        }

        void startRead() throws JSchException {
            this.channel.connect(machineSshModel.timeout());
            I18nThreadUtil.execute(this);
        }

        /**
         * 调整 缓存区大小
         *
         * @param jsonObject 参数
         */
        private void resize(JSONObject jsonObject) {
            Integer rows = Convert.toInt(jsonObject.getString("rows"), 10);
            Integer cols = Convert.toInt(jsonObject.getString("cols"), 10);
            Integer wp = Convert.toInt(jsonObject.getString("wp"), 10);
            Integer hp = Convert.toInt(jsonObject.getString("hp"), 10);
            this.channel.setPtySize(cols, rows, wp, hp);
        }

        /**
         * 添加到命令队列
         *
         * @param msg 输入
         * @return 当前待确认待所有命令
         */
        private String append(String msg) {
            char[] x = msg.toCharArray();
            if (x.length == 1 && x[0] == 127) {
                // 退格键
                int length = nowLineInput.length();
                if (length > 0) {
                    nowLineInput.delete(length - 1, length);
                }
            } else {
                nowLineInput.append(msg);
            }
            return nowLineInput.toString();
        }

        /**
         * 检查输入是否包含禁止命令,记录执行记录
         *
         * @param msg                  输入
         * @param userInfo             用户
         * @param sshCommandNotLimited 是否解除限制
         * @return true 没有任何限制
         */
        public boolean checkInput(String msg, UserModel userInfo, boolean sshCommandNotLimited) {
            String allCommand = this.append(msg);
            boolean refuse;
            // 超级管理员不限制,有权限都不限制
            boolean systemUser = userInfo.isSuperSystemUser() || sshCommandNotLimited;
            if (StrUtil.equalsAny(msg, StrUtil.CR, StrUtil.TAB)) {
                String join = nowLineInput.toString();
                if (StrUtil.equals(msg, StrUtil.CR)) {
                    nowLineInput.setLength(0);
                }
                // sshItem 可能为空
                refuse = sshItem == null || SshModel.checkInputItem(sshItem, join);
            } else {
                // 复制输出
                refuse = sshItem == null || SshModel.checkInputItem(sshItem, msg);
            }
            // 执行命令行记录
            keyEventCycle.read(text -> {
                // 获取基础信息
                Map attributes = session.getAttributes();
                String ip = (String) attributes.get("ip");
                String userAgent = (String) attributes.get(HttpHeaders.USER_AGENT);
                MachineSshModel machineSshModel = (MachineSshModel) attributes.get("machineSsh");
                SshModel sshItem = (SshModel) attributes.get("dataItem");
                sshTerminalExecuteLogService.batch(userInfo, machineSshModel, sshItem, ip, userAgent, refuse, Collections.singletonList(text));
            }, msg.getBytes(Charset.forName(machineSshModel.getCharset())));
            // 执行命令行记录
            // logCommands(session, allCommand, refuse);
            return systemUser || refuse;
        }


        @Override
        public void run() {
            try {
                byte[] buffer = new byte[1024];
                int i;
                //如果没有数据来,线程会一直阻塞在这个地方等待数据。
                while ((i = inputStream.read(buffer)) != NioUtil.EOF) {
                    byte[] tempBytes = Arrays.copyOfRange(buffer, 0, i);
                    keyEventCycle.receive(tempBytes);
                    sendBinary(session, new String(tempBytes, machineSshModel.charset()));
                }
            } catch (Exception e) {
                if (!this.openSession.isConnected()) {
                    log.error(I18nMessageUtil.get("i18n.ssh_error_string.6bdb"), e.getMessage());
                    return;
                }
                log.error(I18nMessageUtil.get("i18n.read_error.7fa5"), e);
                SshHandler.this.destroy(this.session);
            }
        }

        @Override
        public void close() throws Exception {
            IoUtil.close(this.inputStream);
            IoUtil.close(this.outputStream);
            JschUtil.close(this.channel);
            JschUtil.close(this.openSession);
        }
    }

    @Override
    public void destroy(WebSocketSession session) {
        HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
        IoUtil.close(handlerItem);
        IoUtil.close(session);
        HANDLER_ITEM_CONCURRENT_HASH_MAP.remove(session.getId());
        SocketSessionUtil.close(session);
    }

    /**
     * 控制台案件事件处理
     */
    public static class KeyEventCycle {

        // 输入缓存
        private StringBuffer buffer = new StringBuffer();
        // 输入后是否接收返回字符串
        private boolean inputReceive = false;
        // TAB 输入暂停(处理Y/N确认)
        private boolean tabInputPause = false;
        // 光标位置
        private int inputSelection = 0;
        @Setter
        private Charset charset;
        private KeyControl keyControl = KeyControl.KEY_END;

        /**
         * 从控制台读取输入按键进行处理
         * @param consumer 完整命令后输入回调
         * @param bytes 输入按键
         */
        public void read(Consumer consumer, byte... bytes) {
            String str = new String(bytes, charset);
            if (keyControl == KeyControl.KEY_TAB && tabInputPause) {
                if (str.equalsIgnoreCase("y") || str.equalsIgnoreCase("n")) {
                    tabInputPause = false;
                    return;
                }
            }
            keyControl = KeyControl.getKeyControl(bytes);
            if ((keyControl == KeyControl.KEY_INPUT || keyControl == KeyControl.KEY_FUNCTION) && !tabInputPause) {
                buffer.insert(inputSelection, str);
                inputSelection += str.length();
            } else if (keyControl == KeyControl.KEY_ENTER) {
                // 回车,结束当前输入周期
                if (buffer.length() > 0) {
                    consumer.accept(buffer.toString());
                }
                // 重置周期
                buffer = new StringBuffer();
                inputReceive = false;
                inputSelection = 0;
            } else if (keyControl == KeyControl.KEY_BACK) {
                buffer.delete(Math.max(inputSelection - 1, 0), inputSelection);
                inputSelection = Math.max(inputSelection - 1, 0);
            } else if (keyControl == KeyControl.KEY_DELETE) {
                buffer.delete(inputSelection, Math.min(inputSelection + 1, buffer.length()));
            } else if (keyControl == KeyControl.KEY_LEFT) {
                inputSelection = Math.max(inputSelection - 1, 0);
            } else if (keyControl == KeyControl.KEY_RIGHT) {
                inputSelection = Math.min(inputSelection + 1, buffer.length());
            } else if (keyControl == KeyControl.KEY_HOME) {
                inputSelection = 0;
            } else if (keyControl == KeyControl.KEY_END) {
                inputSelection = buffer.length();
            } else if (keyControl == KeyControl.KEY_TAB) {
                inputReceive = true;
            } else if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
                // 清空命令缓冲
                inputSelection = 0;
                inputReceive = true;
            } else if (keyControl == KeyControl.KEY_ETX) {
                buffer = new StringBuffer();
                inputSelection = 0;
            }
        }

        /**
         * 从SSH服务端接收字节
         * @param bytes 字节
         */
        public void receive(byte... bytes) {
            if (inputReceive) {
                String str = new String(bytes, charset);
                if (keyControl == KeyControl.KEY_UP || keyControl == KeyControl.KEY_DOWN) {
                    // 上下键只有第一条是正常的,后面的都是根据第一条进行退格删除再补充的。
                    // 8,8,8,99,100,32,47,112,114,50,111,99,47,
                    try {
                        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                            for (byte aByte : bytes) {
                                if (aByte == 8) {
                                    // 首位是退格键,就执行删除末尾值
                                    buffer.deleteCharAt(Math.max(buffer.length() - 1, 0));
                                } else if (aByte == 27) {
                                    // 遇到【逃离/取消】就跳出循环
                                    break;
                                } else if (aByte != 0) {
                                    outputStream.write(aByte);
                                }
                            }
                            buffer.append(new String(outputStream.toByteArray(), charset));
                        }
                        inputSelection = buffer.length();
                    } catch (Exception e) {
                        log.error("", e);
                    }
                } else {
                    if (keyControl == KeyControl.KEY_TAB) {
                        if (bytes[0] == 7 || Arrays.equals(new byte[]{13, 10}, bytes)) {
                            inputReceive = false;
                            return;
                        }
                        // tab下文件很多
                        if (str.contains("y or n")) {
                            tabInputPause = true;
                            inputReceive = false;
                            return;
                        }
                        // cat 'hello word.txt'
                        // cat hello\ word.txt
                        if (str.split(" ").length > 1 && (!str.contains("'") && !str.contains("\\"))) {
                            inputReceive = false;
                            return;
                        }
                    }
                    // 非上下键输入输入中,如果接受到数据就执行插入数据,根据当前光标位置执行插入
                    // 存在退格,就从光标位置开始删除
                    int backCount = 0;
                    for (byte aByte : bytes) {
                        if (aByte == 8) {
                            buffer.deleteCharAt(inputSelection);
                            backCount++;
                        }
                    }
                    str = new String(Arrays.copyOfRange(bytes, 0, bytes.length - backCount), charset);
                    buffer.insert(inputSelection, str);
                    inputSelection += str.length();
                }
            }
            inputReceive = false;
        }

    }

    /**
     * 功能键枚举
     */
    public enum KeyControl {
        KEY_TAB((byte) 9), // TAB
        KEY_ETX((byte) 3), // Control + C
        KEY_ENTER((byte) 13), // Enter
        KEY_BACK((byte) 127), // 退格键
        KEY_DELETE(new byte[]{27, 91, 51, 126}), // DELETE键
        KEY_LEFT(new byte[]{27, 91, 68}), // 左
        KEY_RIGHT(new byte[]{27, 91, 67}), // 右
        KEY_UP(new byte[]{27, 91, 65}), // 上
        KEY_DOWN(new byte[]{27, 91, 66}), // 下
        KEY_HOME(new byte[]{27, 91, 72}),
        KEY_END(new byte[]{27, 91, 70}),
        KEY_FUNCTION(new byte[]{27, 91}), //其他功能键
        KEY_INPUT(new byte[]{-1}); // 正常输入

        private final byte[] control;

        KeyControl(byte... control) {
            this.control = control;
        }

        public static KeyControl getKeyControl(byte[] bytes) {
            for (KeyControl value : KeyControl.values()) {
                if (Arrays.equals(value.control, bytes)) {
                    return value;
                }
            }
            // 其他功能键
            if (Arrays.equals(KEY_FUNCTION.control, Arrays.copyOf(bytes, 2))) {
                return KEY_FUNCTION;
            }
            // 正常输入
            return KEY_INPUT;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy