基于websocket和java实现webshell访问docker容器
基于websocket和java实现webshell访问docker容器

需求:PaaS平台展示容器信息,并在web端实现与该容器webshell交互。

介绍:通过ws请求与主机建立websocket连接,执行docker exec -i [containerid] /bin/bash命令进入docker容器。进入容器是可以理解为进入进程,通过进程的输入输出流进行交互,并且process的InputStream的在read的时候是一个阻塞,因此在建立连接后启动输入流输出流线程来执行命令和获取结果。前端通过websocket的websocket。send(message)发送执行命令至后台@onmessage通过输出流线程在容器内执行,并且执行结果通过输入流响应到web端。注:需设置process.waitfor();这样输入流阻塞时存在有执行命令结果会响应到webshell,同时输出流在向容器写命令时最后需加上“\n”回车,这样容器才会执行命令

web代码(webshell界面代码从网上down,效果体验很好):


` python

<!DOCTYPE html>
 <html>
  <head>
  <meta charset="utf-8">
  <title>控制台</title>
 <script type="text/javascript" src="js/jquery.js">   </script>
<script type="text/javascript" src="js/jquery- fieldselection.min.js"></script>
<style>
*{
    margin:0px;
    padding:0px;
}
body{
    overflow:hidden;
}
#log-container{
    width:100%;
    height:800px;
}
#log-container textarea.crtP{
    width:100%;
    height:100%;
    background:#000;
    color:#fff;
    font-size: 13px;
    font-family: Lucida Console;
    resize:none;
    border-radius: 6px;
    -webkit-transition: box-shadow 0.30s ease-in-out;
    border: #87C6F9 2px solid;
    box-shadow: 0 0 8px rgba(103, 166, 217, 1);
}
#log-container textarea.crtP::selection {
    background:rgb(0,255,0); 
    color:#000;
}
#log-container textarea.crtP::-moz-selection {
    background:rgb(0,255,0); 
    color:#000;
}
#log-container textarea.crtP::-webkit-selection {
    background:rgb(0,255,0); 
    color:#000;
}

</style>
</head>
 <body>
<div id="log-container">
    <textarea class="crtP" spellcheck="false"></textarea>
</div>
</body>
<script>
$(document).ready(function() {
    var hisCmdArray = new Array();
    var cmdIndex = 0;
    var cmdSize = 0;
    var tmpCmd = "";
    var minRangeLength = 0;
    var termShellStr = "";
    var textareaCont = "";
    var $textarea = $("#log-container textarea.crtP");
    var maxLine = 200;
    var fromX,fromY,toX,toY;
    var TimeFn = null;
    var isDbl = false;
    //var dblCt = 0;
    // 指定websocket路径
    var websocket = new WebSocket(*'ws://192.168.111.133:8006/websockettail/webSecureCRT');*
    websocket.onmessage = function(event) {
        //console.log("返回结果="+event.data);
        var context = "";
        // 接收服务端的实时日志并添加到HTML页面中
        if(event.data.substring(0,9) === "startFlag"){
            termShellStr = event.data.split("=")[1];
            termShellStr = termShellStr.substring(0,termShellStr.length);
            //console.log(termShellStr);
            context = termShellStr;
        }else {
            context = event.data+"\n";
        }
        tmpCmd = "";
        inputCmd($textarea,context);
        minRangeLength = $textarea.val().length;
        //console.log(minRangeLength);
        // 滚动条滚动到最低部
        $textarea.scrollTop($textarea[0].scrollHeight);
    };

    function inputCmd(inputObj,cmd){
        var hisCont = inputObj.val();
        var array = $textarea.val().split("\n");
        if(array.length>=maxLine){
            /* var hasIndex = 0;
            var deleteLength = 0;
            while( array[hasIndex].length === 0){
                deleteLength += array[hasIndex].length;
                hasIndex++;
            }
            deleteLength += array[hasIndex].length;
            console.log(deleteLength);
            hisCont = hisCont.substring(deleteLength);
            console.log(hisCont.length); */
            for(var i=0;i<=array.length-maxLine;i++){
                hisCont = hisCont.substring(array[i].length);
            }
            //console.log(hisCont.length);
        }
        hisCont = hisCont.substring(0,hisCont.length-tmpCmd.length);
        inputObj.val(hisCont+cmd);
    }

    $.fn.setCursorPosition = function(position){  
        if(this.lengh == 0) return this;  
        return $(this).setSelection(position, position);  
    }  

    $.fn.setSelection = function(selectionStart, selectionEnd) {  
        if(this.lengh == 0) return this;  
        input = this[0];  

        if (input.createTextRange) {  
            var range = input.createTextRange();  
            range.collapse(true);  
            range.moveEnd('character', selectionEnd);  
            range.moveStart('character', selectionStart);  
            range.select();  
        } else if (input.setSelectionRange) {  
            input.focus();  
            input.setSelectionRange(selectionStart, selectionEnd);  
        }  

        return this;  
    }  

    $.fn.focusEnd = function(){ 
        this.setCursorPosition(this.val().length);  
    }

    $textarea.click(function(event){
        clearTimeout(TimeFn);
        TimeFn = setTimeout(function(){
            console.log("click");
            isDbl = false;
            if(toX===fromX && toY===fromY && !isDbl){
                $textarea.focusEnd();
            }
        },300);
    });

    $textarea.mouseup(function(event){
        toX = event.pageX;
        toY = event.pageY;
    });

    /* $textarea.mousemove(function(event){
        console.log(event)
        console.log("mousemove");
    }); */

    $textarea.mousedown(function(event){
        //console.log(event.which);
        if(event.which === 2){//如果按下鼠标中键
            var range = $textarea.getSelection();
            console.log(range.text);
            inputCmd($textarea,range.text);
            return false;
        }
        fromX = event.pageX;
        fromY = event.pageY;
    });

    $textarea.dblclick(function(event){
        clearTimeout(TimeFn);
        isDbl = true;
        console.log("dblclick");
    });


    $(document).keydown(function(event){
        //console.log(event.keyCode);
        var range = $textarea.getSelection();
        //console.log(range);

        if(event.keyCode == 13){
            var array = $textarea.val().split("\n");
            //alert(array[array.length-1]);
            //send();
            var message = array[array.length-1];
            if(message.length>=termShellStr.length){
                message = message.substring(termShellStr.length);
                console.log(message);
                if(message === "clear"){
                    $textarea.val(termShellStr);
                    //$textarea.focusEnd();
                    return false;
                }else {
                    send(message);
                }
                if(message === ""){
                }else {
                    hisCmdArray.push(message);
                    cmdIndex = hisCmdArray.length;
                    cmdSize = hisCmdArray.length;
                }
            }else{
                send(message);
            }
            console.log(hisCmdArray);
        }else if(event.keyCode == 8){//删除键
            var array = $textarea.val().split("\n");
            //alert(array[array.length-1]);
            //send();
            var currLine = array[array.length-1];
            //console.log(currLine+"--"+termShellStr);
            if(currLine.substring(0,termShellStr.length) === termShellStr){
            }else {
                return false;
            }
            if(currLine === termShellStr){
                return false;
            }

        }else if(event.keyCode == 38){//向上键
            cmdIndex--;
            if(cmdIndex < 0){
                cmdIndex = 0;
                return false;
            }
            if(cmdIndex < cmdSize && cmdIndex >= 0){
                var lastCmd = hisCmdArray[cmdIndex];
                console.log(lastCmd);
                inputCmd($textarea,lastCmd);
                tmpCmd = lastCmd;
            }
            return false;
        }else if(event.keyCode == 40){//向下键
            cmdIndex++;
            if(cmdIndex >= cmdSize){
                cmdIndex = cmdSize-1;
                return false;
            }
            if(cmdIndex < cmdSize && cmdIndex >= 0){
                var nextCmd = hisCmdArray[cmdIndex];
                console.log(nextCmd);
                inputCmd($textarea,nextCmd);
                tmpCmd = nextCmd;
            }
            return false;
        }else if(event.keyCode == 37){//向左键
            //console.log(minRangeLength);
            if(range.end > minRangeLength){

            }else {
                return false;
            }
        }

        if (event.keyCode == 67 && event.ctrlKey) {  //这里只能用alt,shift,ctrl等去组合其他键event.altKey、event.ctrlKey、event.shiftKey 属性
            send("Ctrl+C");
        } 
    });


    //发送消息
    function send(message){
        websocket.send(message);
    }

    $textarea.focusEnd();

    /* $("#log-container textarea").change( function() {
        var array = $("#log-container textarea").val().split("\n");
        var currLine = array[array.length-1];
        console.log(currLine+"--"+termShellStr);
        if(currLine === termShellStr){
            return false;
        }
    }); */
});
</script>
 </body>
 </html>

后台代码与容器建立进程,启动输入线程和输出线程进行交互:

package com.xxg.websocket;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/webSecureCRT")
public class WebSecureCRT {
    protected Runtime runtime = Runtime.getRuntime();
    protected Process process;
    protected InputStream inputStream;
    protected InputStream errStream;
    public static String termStr = "pp=`whoami`'@'`hostname`':'`pwd`'>' ";
    public String pwd = "";
    protected String termShowStr = "";
    public static String pwdCmdStr = "tt='pwd='`pwd` && echo $tt";
    public static String hostType = "/bin/sh -c"; //unix /usr/bin/ksh
    public static String hhStr = "\n";
    protected Map<String,String> envMap = System.getenv();
    protected String[] envStrArray;

    public static List<String> cmdList = new ArrayList<String>();

    static {
        cmdList.add("tail");
        cmdList.add("more");
    }

    public WebSecureCRT() {
        super();
        System.out.println("创建WebSecureCRT对象");
    }   
  /**
       * 新的WebSocket请求开启
       */
              @OnOpen
                    public void onOpen(Session session) {
                        String message="pp=`whoami`'@'`hostname` && echo $pp ";
                        try {
                        System.out.println("connect to server start...");
                        String command = "docker exec -i b2873e708667 /bin/bash ";
                        String[] commandArray = {"/bin/sh","-c",command};
                        process =Runtime.getRuntime().exec(commandArray,null);
                        System.out.println(process);
                        onMessage(message, session);

                   } catch (Exception e) {
            e.printStackTrace();
        }
    }


/**
 * WebSocket请求关闭
 */
@OnClose
public void onClose() {
    try {
        if(inputStream != null)
            inputStream.close();
        if(errStream != null)
            errStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
    if(process != null)
        process.destroy();
}

@OnError
public void onError(Throwable thr) {
    thr.printStackTrace();
}

/**
 * 收到客户端消息后调用的方法
 * @param message 客户端发送过来的消息
 * @param session 可选的参数
 * @throws ExecutionException 
 * @throws InterruptedException 
 * @throws Exception 
 */
@OnMessage
        public void onMessage(String message, Session session) throws InterruptedException, ExecutionException, Exception {
            System.out.println("from clinet message start [" + message+"]");
            System.out.println(process);
            StreamOutput outGobbler = new StreamOutput(process.getOutputStream(), "OUTPUT",message);  
            outGobbler.start();

    StreamGobbler infoGobbler = new  StreamGobbler(process.getInputStream(), "INPUT",session);  
    infoGobbler.start();

    StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR",session);  
    errorGobbler.start();
    System.out.println("from clinet message end...[" + message+"]");
}

}

输入流线程代码读取流响应到前端:

package com.xxg.websocket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

import javax.websocket.Session;

/**
* 用于处理Runtime.getRuntime().exec产生的错误流及输出流
* @author shaojianye
*
*/
public class StreamGobbler extends Thread {
InputStream is;
String type;
Session session;

StreamGobbler(InputStream is, String type,Session session) { 
    this.is = is; 
    this.type = type; 
    this.session=session;
} 
public void run() { 
    try { 
        InputStreamReader isr = new InputStreamReader(is); 
        BufferedReader reader = new BufferedReader(isr); 
        String line=null; 
        if("INPUT".equals(type)){
            while((line = reader.readLine()) != null) {
                 System.out.println("---------");
                 System.out.println("in:"+line);
                 if(line.length()==0){
                     session.getBasicRemote().sendText("\n"); 
                 }else{
                     session.getBasicRemote().sendText(line);
                }
            }
        }
        if("ERROR".equals(type)){
             while((line = reader.readLine()) != null) {
                 System.out.println("in:"+line);
                 if(line.length()==0){
                     session.getBasicRemote().sendText("\n"); 
                 }else{
                     session.getBasicRemote().sendText(line);
                 }
            }
        }
    } catch (Exception ioe) { 
        ioe.printStackTrace();   
    } 
} 
} 

获取前端传入linux命令通过输出流线程响应到容器

package com.xxg.websocket;

import java.io.OutputStream;
import java.io.PrintWriter;

/**
* 用于处理Runtime.getRuntime().exec产生的错误流及输出流
* @author shaojianye
*
*/
public class StreamOutput extends Thread {
OutputStream out ;
String type;
String message;

StreamOutput(OutputStream out, String type,String message) { 
    this.out = out; 
    this.type = type; 
    this.message=message;
} 
public void run() { 
    try { 
        PrintWriter pw = null;
        String val=message+" \n";
        System.out.println(val);
        //判断exit是否可以退出,待测试
        if (out!=null){
            pw = new PrintWriter(out); 
            pw.println(val);
            pw.flush();
        } 
      } catch (Exception ioe) { 
        ioe.printStackTrace();   
    } 
  } 
} 

部署服务结果如图:

因为执行的docker exec -i containerid /bin/bash命令,没有-t参数(查相关资料-t参数在io流传输是会失效具体原因不太清楚)因此响应的webshell没有虚拟化终端样式
这里写图片描述

测试demo在github地址https://github.com/SHAOLBJ/Webshell

访问宿主机的效果实现思路基本一致:

这里写图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐