前言最近工作需要开发远程虚拟机前端运行的功能,简称WebShell。基于目前react+django的技术栈,研究了一段时间,发现后端实现大多是django+channels来实现websocket服务。大体一看,觉得这还不够有趣。翻了django的官方文档,发现django本身是不支持websocket的,但是django3支持asgi协议后,可以自己实现websocket服务。于是选择了gunicorn+uvicorn+asgi+websocket+django3.2+paramiko来实现WebShell。实现websocket服务,django内置脚手架生成的工程会自动生成asgi.py和wsgi.py两个文件。大部分常见的应用都是使用wsgi.py配合nginx部署在线服务。这次使用asgi.py实现websocket服务的主要思路在网上搜索了一下,主要是实现connect/send/receive/disconnect这几个action的处理方法。这里HowtoAddWebsocketstoaDjangoAppwithoutExtraDependencies就是一个很好的实例,但超过了简单……思路#asgi.pyimportosfromdjango.core.asgiimportget_asgi_applicationfromwebsocket_app.websocketimportwebsocket_applicationos.environ.setdefault_setting,SJANGODULESETTING('DJANGODULESET'TING)django_application=get_asgi_application()asyncdeapplication(scope,receive,send):ifscope['type']=='http':awaitdjango_application(scope,receive,send)elifscope['type']=='websocket':awaitwebsocket_application(scope,接收,发送)否则:raiseNotImplementedError(f“Unknownscopetype{scope['type']}”)#websocket.pyasyncdefwebsocket_application(范围,接收,发送):pass#websocket.pyasyncdefwebsocket_application(范围,接收,发送):whileTrue:事件=awaitreceive()ifevent['type']=='websocket.connect':awaitsend({'type':'websocket.accept'})ifevent['type']=='websocket.disconnect':breakifevent['type']=='websocket.receive':ifevent['text']=='ping':awaitsend({'type':'websocket.send','text':'pong!'})实际上面的代码提供了思路其中最核心的实际部分我放下面:classWebSocket:def__init__(self,scope,receive,send):self._scope=scopeself._receive=receiveself._send=sendself._client_state=State.CONNECTINGself._app_state=State.CONNECTING@propertydefheaders(self):returnHeaders(self._scope)@propertydefscheme(self):returnsself._scope["scheme"]@propertydefpath(self):returnsself._scope["path"]@propertydefquery_params(self):returnQueryParams(self._scope["query_string"].decode())@propertydefquery_string(self)->str:returnsself._scope["query_string"]@propertydefscope(self)):returnsself._scopeasyncdefaccept(self,subprotocol:str=None):"""Acceptconnection.:paramsubprotocol:Thesubprotocoltheserverwishestoaccept.:typesubprotocol:str,optional"""ifself._client_state==State.CONNECTING:awaitself.receive()awaitself。发送({“类型”:SendEvent.ACCEPT,“子协议”:子协议})asyncdefclose(自我,代码:我nt=1000):awaitself.send({"type":SendEvent.CLOSE,"code":code})asyncdefsend(self,message:t.Mapping):ifself._app_state==State.DISCONNECTED:raiseRuntimeError("WebSocketisdisconnected.")ifself._app_state==State.CONNECTING:assertmessage["type"]in{SendEvent.ACCEPT,SendEvent.CLOSE},('Couldnotwriteevent"%s"intosocketinconnectingstate.'%message["type"])ifmessage["type"]==SendEvent.CLOSE:self._app_state=State.DISCONNECTEDelse:self._app_state=State.CONNECTDelifself._app_state==State.CONNECTED:assertmessage["type"]in{SendEvent.SEND,SendEvent.CLOSE},('已连接的套接字可以发送“%s”和“%s”事件,而不是“%s”'%(SendEvent.SEND,SendEvent.CLOSE,message["type"]))ifmessage["type"]==SendEvent.CLOSE:self._app_state=State.DISCONNECTEDawaitself._send(message)asyncdefreceive(self):ifself._client_state==State.DISCONNECTED:raiseRuntimeError("WebSocketisdisconnected.")message=awaitself._receive()ifself._client_state==State.CONNECTING:assertmessage["type"]==ReceiveEvent.CONNECT,('WebSocketisinconnectingstatebutreceived'%s"event'%message["type"])self._client_state=State.CONNECTEDelifself._client_state==State.CONNECTED:assertmessage["type"]in{ReceiveEvent.RECEIVE,ReceiveEvent.DISCONNECT},('WebSocketisconnectedbutreceivedininvalidevent"%s".'%message["type"])ifmessage["type"]==ReceiveEvent.DISCONNECT:self._client_state=State.DISCONNECTEDreturnmessage作为一个合格的代码搬运工,stitchmonster为了提高处理效率还是需要造一些轮子来填坑的。如何结合上面的WebSocket类和paramiko起来接受前端到远程主机的字符,同时接受返回?importasyncioimporttracebackimportparamikofromwebshel??l.sshimportBase,RemoteSSHfromwebshel??l.connectionimportWebSocketclassWebShell:"""组织WebSocket和paramiko.Channel,实现da两者之间的交换"""def__init__(self,ws_session:WebSocket,ssh_session:paramiko.SSHClient=None,chanel_sessionsion:paramiko.Channel=None):self.ws_session=ws_sessionself.ssh_session=ssh_sessionself.chanel_session=chanel_sessiondefinit_ssh(self,host=None,port=22,user="admin",passwd="admin@123"):self.ssh_session,self.chanel_session=RemoteSSH(主机,端口,用户,密码).session()defset_ssh(self,ssh_session,chanel_session):self.ssh_session=ssh_sessionself.chanel_session=chanel_sessionasyncdefready(self):awaitself.ws_session.accept()asyncdefwelcome(self):#展示Linux欢迎相关内容foriinrange(2):ifself.chanel_session.send_ready():message=self.chanel_session.recv(2048).decode('utf-8')ifnotmessage:returnawaitself.ws_session.send_text(message)asyncdefweb_to_ssh(self):#print('--------web_to_ssh------>')whileTrue:#print('--------------->')ifnotself.chanel_session.activeornotself.ws_session.status:returnawaitasyncio.sleep(0.01)shell=awaitself.ws_session.receive_text()#print('-------shell-------->',shell)ifself.chanel_session.activeandself.chanel_session.send_ready():self.chanel_session.send(bytes(shell,'utf-8'))#print('------------>',"end")asyncdefssh_to_web(self):#print('<--------ssh_to_web------------')whileTrue:#print('<-----------------')ifnotself.chanel_session.active:awaitself.ws_session.send_text('sshclosed')returnifnotself.ws_session.status:returnawaitasyncio.sleep(0.01)ifnotself.chanel_session.recv_ready():message=self.chanel_session.recv(2048).decode('utf-8')#print('<--------message----------',message)ifnotlen(message):continueawaitself.ws_session。send_text(消息)#print('<----------------',"end")asyncdefrun(self):ifnotself.ssh_session:raiseException("sshnotinit!")awaitself.ready()awaitasyncio.gather(self.web_to_ssh(),self.ssh_to_web())defclear(self):try:self.ws_session.close()exceptException:traceback.print_stack()try:self.ssh_session.close()exceptException:traceback.print_stack()前端xterm.js完全满足,搜索下找个看简单的就行exportclassTermextendsReact.Component{privateterminal!:HTMLDivElement;privatefitAddon=newFitAddon();componentDidMount(){constxterm=newTerminal();xterm.loadAddon(this.fitAddon);xterm.loadAddon(newWebLinksAddon());//usingwssforhttps//constsocket=newWebSocket("ws://"+window.location.host+"/api/v1/ws");constsocket=newWebSocket("ws://localhost:8000/webshel??l/");//socket.onclose=(event)=>{//this.props.onClose();//}socket.onopen=(event)=>{xterm.loadAddon(newAttachAddon(socket));this.fitAddon.fit();xterm.focus();}xterm.open(this.terminal);xterm.onResize(({cols,rows})=>{socket.send("
