スキーム
このスキームは、3つの主要コンポーネントに基づいています。
- ボットサーバー :HTTPリクエストを含むメッセージを受信し、実行し、エンコードして、クライアントに結果を送信します
- ボットクライアント :実行する必要があるHTTPリクエストに関するサーバー情報を送信し、結果を待機し、リクエストの結果を処理して、さらに使用できる状態に戻します
- http-proxy :ボットクライアントを使用してHTTP要求を処理するプロキシサーバー
コンポーネントは次のように配置されます。インターネットにアクセスできるリモートマシン上で、ボットサーバーが起動します。 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ボットの「非対話型」アプリケーションを実証するのに十分なはずです。
プロジェクトでは、認証の追加、リクエストタイプの通常のサポート、パフォーマンスの改善など、改善できる点がたくさんあります。 このようなアーキテクチャでどのようなパフォーマンスを達成できるかは非常に興味深いです。その研究については、おそらくすぐに取り上げます。