最近工作中需要开发前端操作远程虚拟机的功能,简称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这个几个动作的处理方法。
这里howtoaddwebsocketstoadjangoappwithoutextradependencies(
https://jaydenwindle.com/writing/django-websockets-zero-dependencies/)就是一个很好的实例,但过于简单……
#asgi.py
importos
fromdjango.core.asgiimportget_asgi_application
fromwebsocket_app.websocketimportwebsocket_application
os.environ.setdefault(\'django_settings_module\',\'websocket_app.settings\')
django_application=get_asgi_application()
asyncdefapplication(scope,receive,send):
ifscope[\'type\']==\'http\':
awaitdjango_application(scope,receive,send)
elifscope[\'type\']==\'websocket\':
awaitwebsocket_application(scope,receive,send)
else:
raisenotimplementederror(f\"unknownscopetype{scope[\'type\']}\")
#websocket.py
asyncdefwebsocket_application(scope,receive,send):
pass
#websocket.py
asyncdefwebsocket_application(scope,receive,send):
whiletrue:
event=awaitreceive()
ifevent[\'type\']==\'websocket.connect\':
awaitsend({
\'type\':\'websocket.accept\'
})
ifevent[\'type\']==\'websocket.disconnect\':
break
ifevent[\'type\']==\'websocket.receive\':
ifevent[\'text\']==\'ping\':
awaitsend({
\'type\':\'websocket.send\',
\'text\':\'pong!\'
})实现
上面的代码提供了思路,比较完整的可以参考这里websockets-in-django-3-1(
https://aliashkevich.com/websockets-in-django-3-1/)基本可以复用了。
其中最核心的实现部分我放下面:
classwebsocket:
def__init__(self,scope,receive,send):
self._scope=scope
self._receive=receive
self._send=send
self._client_state=state.connecting
self._app_state=state.connecting
@property
defheaders(self):
returnheaders(self._scope)
@property
defscheme(self):
returnself._scope[\"scheme\"]
@property
defpath(self):
returnself._scope[\"path\"]
@property
defquery_params(self):
returnqueryparams(self._scope[\"query_string\"].decode())
@property
defquery_string(self)->str:
returnself._scope[\"query_string\"]
@property
defscope(self):
returnself._scope
asyncdefaccept(self,subprotocol:str=none):
\"\"\"acceptconnection.
:paramsubprotocol:thesubprotocoltheserverwishestoaccept.
:typesubprotocol:str,optional
\"\"\"
ifself._client_state==state.connecting:
awaitself.receive()
awaitself.send({\"type\":sendevent.accept,\"subprotocol\":subprotocol})
asyncdefclose(self,code:int=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.disconnected
else:
self._app_state=state.connected
elifself._app_state==state.connected:
assertmessage[\"type\"]in{sendevent.send,sendevent.close},(
\'connectedsocketcansend\"%s\"and\"%s\"events,not\"%s\"\'
%(sendevent.send,sendevent.close,message[\"type\"])
)
ifmessage[\"type\"]==sendevent.close:
self._app_state=state.disconnected
awaitself._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.connected
elifself._client_state==state.connected:
assertmessage[\"type\"]in{receiveevent.receive,receiveevent.disconnect},(
\'websocketisconnectedbutreceivedinvalidevent\"%s\".\'
%message[\"type\"]
)
ifmessage[\"type\"]==receiveevent.disconnect:
self._client_state=state.disconnected
returnmessage缝合怪
做为合格的代码搬运工,为了提高搬运效率还是要造点轮子填点坑的,如何将上面的websocket类与paramiko结合起来,实现从前端接受字符传递给远程主机,并同时接受返回呢?
importasyncio
importtraceback
importparamiko
fromwebshell.sshimportbase,remotessh
fromwebshell.connectionimportwebsocket
classwebshell:
\"\"\"整理websocket和paramiko.channel,实现两者的数据互通\"\"\"
def__init__(self,ws_session:websocket,
ssh_session:paramiko.sshclient=none,
chanel_session:paramiko.channel=none
):
self.ws_session=ws_session
self.ssh_session=ssh_session
self.chanel_session=chanel_session
definit_ssh(self,host=none,port=22,user=\"admin\",passwd=\"admin@123\"):
self.ssh_session,self.chanel_session=remotessh(host,port,user,passwd).session()
defset_ssh(self,ssh_session,chanel_session):
self.ssh_session=ssh_session
self.chanel_session=chanel_session
asyncdefready(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:
return
awaitself.ws_session.send_text(message)
asyncdefweb_to_ssh(self):
#print(\'--------web_to_ssh------->\')
whiletrue:
#print(\'--------------->\')
ifnotself.chanel_session.activeornotself.ws_session.status:
return
awaitasyncio.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\')
return
ifnotself.ws_session.status:
return
awaitasyncio.sleep(0.01)
ifself.chanel_session.recv_ready():
message=self.chanel_session.recv(2048).decode(\'utf-8\')
#print(\'<---------message----------\',message)
ifnotlen(message):
continue
awaitself.ws_session.send_text(message)
#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/webshell/\");
//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(\"<resize>\"+cols+\",\"+rows)
});
window.addeventlistener(\'resize\',this.onresize);
}
componentwillunmount(){
window.removeeventlistener(\'resize\',this.onresize);
}
onresize=()=>{
this.fitaddon.fit();
}
render(){
return<divclassname=\"terminal\"ref={(ref)=>this.terminal=refashtmldivelement}></div>;
}
}
上邦文化旅游网
山东自考之家