通常のPython XMPPボットではない:トンネリング

少し前に、 PythonのICQについての記事が公開されました。これにより、トピックを少しずつ開発するように促されました。 数年前、ホームインターネットに問題がありました。外部ネットワークとの通信からのみローカルネットワークへのアクセス、ICQとローカルJabberサーバーのみ。 他の方法はありませんでした。 その結果、XMPPでHTTPトラフィックをトンネリングするというアイデアが生まれました。







スキーム



このスキームは、3つの主要コンポーネントに基づいています。







コンポーネントは次のように配置されます。インターネットにアクセスできるリモートマシン上で、ボットサーバーが起動します。 localhostで、ボットクライアントとプロキシが起動します。 クライアントアプリケーションは、たとえば次のようにプロキシを使用するように構成されます。



$ http_proxy="localhost:3128" wget ...









単純なXMLベースのプロトコルを使用して、ボットクライアントとボットサーバーをやり取りします。



example.comインデックスページのダウンロードリクエスト:

 <url>http://example.com</url>
      
      







答えは:

 <answer chunk="2" count="19"><data>encoded_data</data></answer>
      
      







答えはいくつかの部分、chunk'ovで構成されています。 ここで、 chunkチャンクの数、 countはリクエストへの応答が分割されたチャンクの総数です。 encoded_data -base64でエンコードされた応答ピース。



より明確にするために、図をグラフィカルに表示します。



                                     ローカル                                            
 + ------------------------------------------------- ---------------------------------- +
 |  http-client(ブラウザ、wget)-> http-proxy-> bot-client | 
 + ------------------------------------------------- ---------------------------------- +
                                        / \
                                        ||
                                        \ /
                                    遠い
 + ------------------------------------------------- ---------------------------------- +
 | ボットサーバー|
 + ------------------------------------------------- ---------------------------------- +




実装



一般的な情報


XMPPでの作業には、 xmpppyが使用されます。 トリッキーな機能は必要ありません。着信メッセージを処理して返信を送信するだけです。 XMLは、標準ライブラリxml.dom.minidomを使用して解析および生成されます



ボットサーバー


サーバーのタスクは、ダウンロード要求を受信し、ライブラリに送信することです。ライブラリ自体は、ダウンロードする必要があるものを見つけて結果を返し、サーバーはこの結果をクライアントに転送します。



簡略化されたスキームでは、サーバー側のメッセージ処理は次のようになります。



 import xmpp from Fetcher import Fetcher fetcher = None def message_callback(con, msg): global fetcher if msg.getBody(): try: ret = fetcher.process_command(msg.getBody()) except: ret = ["failed to process command"] for i in ret: reply = xmpp.Message(msg.getFrom(), i) reply.setType('chat') con.send(reply) if __name__ == "__main__": jid = xmpp.JID("my@server.jid") user = jid.getNode() server = jid.getDomain() password = "secret" conn = xmpp.Client(server, debug=[]) conres = conn.connect() authres = conn.auth(user, password, resource="foo") conn.RegisterHandler('message', message_callback) conn.sendInitPresence() fetcher = Fetcher() while True: conn.Process(1)
      
      







コードをよりコンパクトで読みやすくするために、エラー処理とハードコードされた値を意図的に削除しました。 ここで何が起こっているのでしょうか? jabberサーバーに接続し、メッセージハンドラーを切断します。



  conn.RegisterHandler('message', message_callback)
      
      







したがって、新しい着信メッセージごとに、 message_callback(con、msg)関数が呼び出され、その引数は接続ハンドルとメッセージ自体になります。 関数自体はFetcherクラスからコマンドハンドラーを呼び出します。Fetcherクラスはすべての「ダーティ」な作業を行い、クライアントに与えられたチャンクのリストを返します。 それだけです。これがサーバーの終了点です。



フェッチャー


Fetcherクラスは、HTTP要求を実行およびエンコードするまさにそのロジックを実装します。 コード全体を説明するのではなく、記事に添付されているアーカイブで確認できます。主なポイントのみを説明します。



  def process_command(self, command): doc = xml.dom.minidom.parseString(command) url = self._gettext(doc.getElementsByTagName("url")[0].childNodes) try: f = urllib2.urlopen(url) except Exception, err: return ["%s" % str(err)] lines = base64.b64encode(f.read()) ret = [] chunk_size = 1024 x = 0 n = 1 chunk_count = (len(lines) + chunk_size - 1) / chunk_size while x < len(lines): ret.append(self._prepare_chunk(n, chunk_count, lines[x:x + chunk_size])) x += chunk_size n += 1 return ret
      
      







ご存知のように、 process_command関数はボットサーバーによって呼び出されます。 XMLリクエストを解析し、リクエストする必要があるURLを決定し、 urllib2でこれを行います。 ダウンロードされたファイルはbase64でエンコードされるため、特殊文字で予期しない問題が発生することはありません。また、メッセージの長さ制限に達しないように均等な部分に分割されます。 次に、各チャンクはXMLでラップされて送信されます。



お客様


実際、クライアントはデータを接着してbase64からデコードする1つのコールバックにすぎません。



 def message_callback(con, msg): global fetcher, output, result if msg.getBody(): message = msg.getBody() chunks, count, data = fetcher.parse_answer(message) output.append(data) if chunks == count: result = base64.b64decode(''.join(output))
      
      







プロキシ


トンネルを透過的に使用するために、HTTPプロキシが実装されています。 プロキシサーバーはポート3128 / tcpにバインドし、要求を待ちます。 受信したリクエストは処理のためにボットサーバーに送信され、結果がデコードされてクライアントに送信されます。 クライアントアプリケーションの観点から見ると、プロキシは「通常の」プロキシと変わりません。



TCPサーバーを作成するには、標準ライブラリのSocketServer.StreamRequestHandlerを使用します。



 class RequestHandler(SocketServer.StreamRequestHandler): def handle(self): data = self.request.recv(1024) method, url, headers = parse_http_request(data) if url is not None: response = fetch_file(server_jid, client_jid, password, url) self.wfile.write(response) self.request.close()
      
      







parse_http_request()関数はHTTPリクエストを解析し、そこからURL、ヘッダー、httpバージョンを取得します。 fetch_file() -ボットクライアントを使用してURLを要求します。



おわりに



完全なソースコードは、sharアーカイブとしてここから入手できます(ファイルを実行し、シェルスクリプトとして実行する必要があります)。 もちろん、これは本格的なアプリケーションというよりはプロトタイプですが、プロトタイプは機能しており、少なくとも小さなファイルを問題なくダウンロードできます。 これは、記事の主な目的、つまりIMボットの「非対話型」アプリケーションを実証するのに十分なはずです。



プロジェクトでは、認証の追加、リクエストタイプの通常のサポート、パフォーマンスの改善など、改善できる点がたくさんあります。 このようなアーキテクチャでどのようなパフォーマンスを達成できるかは非常に興味深いです。その研究については、おそらくすぐに取り上げます。



All Articles