WebSSHっぽいことをしてみる。
WebSocket経由でSSHの操作を行います。 といっても、今回行うのはChromeのSource ShellのようにWebブラウザをSSHとして使うのではなく、 あくまでも、ウェブ経由でサーバ側のSSHプロセスを操作するというものです。
今回参考にしたのは、githubにあるこのリポジトリ
https://github.com/aluzzardi/wssh
WSSHってなんかマルウェアの名前みたい、というか使い方によってはまるっきりマルウェアなんでしょうけど...
ここで必要なの物は以下の3つ、なんとすべてpythonパッケージで構築ができます。
・Webサーバ -> Flask
・WebSocket -> genevt
・sshクライアント -> paramiko
Flaskは軽量なWebフレームワークライブラリ(有名どころでいうと、djangoやRuby on Rails見たいなもの)
geventは並行ライブラリ、socketを利用したネットワークアプリケーションを作成するときに便利です。
paramikoはpythonでSSH接続ができるようになるライブラリ、pythonのttyライブラリと組み合わせることで、
linuxのSSHクライアントと同等の動作が可能になります。
Flask
基本的な使い方はこう
from flask import Flask, request, abort, render_template from jinja2 import FileSystemLoader import os app = Flask(__name__) #インスタンスの作成 @app.route('/')#ルートにアクセスした場合の処理 def index(): return render_template('index.html') #index.htmlがブラウザに表示される。 if __name__ == "__main__": #rootpathの指定と、何かしらファイルを参照する際のtemplatesフォルダを指定する。 #index.htmlはtemplatesフォルダに入れておくと、render_template(~~~)で簡単に参照できる。 root_path = os.path.dirname('/') app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates')) app.run() #サーバの立ち上げ
デフォルトではlocalhost:5000向けにサーバが立ち上がります。 テンプレートディレクトリの指定などはdjangoに似ていてわかりやすいです。
gevent
pythonでウェブソケットを実装しようとして始めて知ったが、このgeventがかなり使いやすいです。 基本的には、複数の処理を並列して実行可能にするライブラリと考えて良いと思います。 バックエンドで何らかの待ちうけ処理を行い、かつ、その裏で他のプログラムを実行したい場合に超有効。 ネットワークアプリなんかは典型的な例です。 今回はwebsocket -> sshと ssh -> websocketの入出力をgeventで並列実行する。 簡単に使い方を紹介。
from flask import Flask, request, abort, render_template from werkzeug.exceptions import BadRequest from gevent import monkey monkey.patch_all() import gevent import socket import time app = Flask(__name__) class WSSHBridge(object): def __init__(self,websocket): self._websocket = websocket self._tasks = [] def _forward_inbound(self): try: while True: data = self._websocket.receive() if not data: return print data finelly: self.close() def _forward_outbound(self): try: while True: time.sleep(5) self._websocket.send("return message") def _bridge(self): self._tasks = [ gevent.spawn(self._forward_inbound), gevent.spawn(self._forward_outbound) ] gevent.joinall(self._tasks) @app.route('/') def index(): bridge = WSSHBridge(request.environ['wsgi.websocket']) bridge._bridge() if __name__ == "__main__": #app.run() from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler from jinja2 import FileSystemLoader import os root_path = os.path.dirname('/') app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates')) http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler) try: http_server.serve_forever() except KeyboardInterrupt: pass
上記はwebsocketでクライアントから送られてくるメッセージをprintで標準出力し、サーバ側は5秒おきにOKの返事を返す仕組みです。
geventで並列動作しているので、両者が独立して動くきます。
(何のためのプログラムなんでしょうか...まあなんかいろいろできそうですね。)
wscatを使えば簡単にウェブソケットを試せるのでお勧めです。
(ちょっと試験したいだけなのに、javascriptで書くはめんどくさいので。。。)
wscat --connect ws://localhost:5000/
ssh
いよいよSSH接続をして見ます。
pythonでSSH接続をするにはparamikoがお勧めです。
何よりも簡単だし、SSHのプロセスを理解していない私でも使えます。
ちなみに、paramikoを使えばSCPやSFTPなんかもできちゃいます。
下記は指定のサーバにssh接続して、コマンドを実行する例。
import paramiko from paramiko import PasswordRequiredException from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.ssh_exception import SSHException def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None): pkey=None self._ssh.connect( hostname=hostname, port=port, username=username, password=password, pkey=pkey, timeout=timeout, allow_agent=allow_agent, look_for_keys=False) ssh = pramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) open('localhost',22,'root','password') stdin, stdout, stderr = ssh.exec_command('ls -l')
set_missing_host_key_policyは接続した際に、サーバホストキーがなかった場合の挙動を定義しています。
よく、最初にサーバへsshするときに、「~~~~yes no」みたいに聞かれますよね?
あれです。今回はここで処理がとまると面倒なので、paramiko.AutoAddPolicy()で自動的にホストキーを追加しています。
いつもの質問に自動的にyesって答えるということです。
connect()でSSHのコネクションを張ります。
connectの中のパラメータはサーバ側に設定したものにあわせて設定、今回は鍵の設定をしていないのでほとんどNoneですが
最後、コマンドの実行はssh.exec_command()です。
実行結果はstdoutに帰ってきます。
Websocket経由でSSH
パーツが揃ったので、いよいよ本題の「WebSocket経由でSSH」を実行してみます。
といってもやることは簡単で、websocketで受け取った入力をコマンドとしてparamikoで実行して、
その結果をまたwebsocketでクライアントに返すだけです。
下記のプログラムを実行すると、5000番ポートでHTTPリクエストを待ち受け、ブラウザ(index.html)とpythonプロセスとの間でwebsocketを使ったやり取りをしています。
from gevent import monkey monkey.patch_all() from flask import Flask, request, abort, render_template from werkzeug.exceptions import BadRequest import gevent from gevent.socket import wait_read, wait_write import paramiko from paramiko import PasswordRequiredException from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.ssh_exception import SSHException import socket import time app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/web/') def webS(): bridge = WSSHBridge(request.environ['wsgi.websocket']) bridge.execute() class WSSHBridge(object): def __init__(self,websocket): self._websocket = websocket self._ssh = paramiko.SSHClient() self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.open('localhost',22,'root','password') self._transport = self._ssh.get_transport() self._channel = self._transport.open_session() self._channel.get_pty('xterm') self._tasks = [] def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None): pkey = None self._ssh.connect( hostname=hostname, port=port, username=username, password=password, pkey=pkey, timeout=timeout, allow_agent=allow_agent, look_for_keys=False) def _forward_inbound(self): try: while True: data = self._websocket.receive() self._channel = self._transport.open_session() self._channel.get_pty('xterm') self._channel.exec_command(data) while True: data = self._channel.recv(1024) self._websocket.send(data) if len(data) == 0: break finally: self.close() def _bridge(self): self._channel.setblocking(False) self._channel.settimeout(0.0) self._tasks = [ gevent.spawn(self._forward_inbound) ] gevent.joinall(self._tasks) def execute(self): self._bridge() self._channel.close() def close(self): gevent.killall(self._tasks, block = True) self._tasks = [] self._ssh.close() if __name__ == "__main__": from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler from jinja2 import FileSystemLoader import os root_path = os.path.dirname('/') app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates')) http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler) try: http_server.serve_forever() except KeyboardInterrupt: pass
さらに、このpythonプロセスとwebsocketで通信するjavascriptのプログラム(index.html)が以下
これはただ単純にwebsocketのチャットプログラムで、チャット相手がSSHサーバになったと考えればよい感じ。
<!DOCTYPR html> <html> <head> <title>WebSSH</title> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script type="text/javascript"> var uri = "ws://"+ location.host +"/web/"; console.log(uri); var webSocket = null; function init(){ $("[data-name='message']").keypress(press); open(); } function open(){ if(webSocket == null){ webSocket = new WebSocket(uri); webSocket.onopen = onOpen; webSocket.onmessage=onMessage; webSocket.onclose = onClose; webSocket.onerror = onError; } } function onOpen(event){ chat("connected"); } function onMessage(event){ if(event && event.data){ chat(event.data); } } function onError(event){ chat("error"); } function onClose(event){ chat("connection closed. "+ event.code); webSocket = null; setTimeout("open()",3000); } function press(event){ if(event && event.which == 13){ $("[data-name='chat']").append("<div>root@ubuntu# "+$("[data-name='message']").val()+"</div>"); var message = $("[data-name='message']").val(); if(message && webSocket){ webSocket.send(""+message); $("[data-name='message']").val(""); } } } function chat(message){ var chats = $("[data-name='chat']").find("div"); while (chats.length >= 100){ chats = chats.last().remove(); } var msgtag = $('<div>').text(message); $("[data-name='chat']").append(msgtag); } $(init); </script> </head> <style> body{ background-color:#000000; color:#00ff00; font-family:'arial black'; } input[type="text"], textarea { background-color : #000000; color:#00ff00; font-family:'arial black'; } input{ border:#000000; } </style> <body> <div data-name="chat"></div> root@ubuntu# <input type="text" data-name="message" size="100"/> <hr /> </body> </html>
こんな感じで動きます。ttyを転送してるわけじゃないから、viエディタとか使えませんけどね...