WEBrickでプロキシサーバを作って遊ぶ

WEBrick で作るコンテンツフィルタ

コンテンツフィルタってなに?

コンテンツフィルタというのは、コンテンツをフィルタリングするソフトウェアです。バナー広告、JavaScript、ポップアップウインドウといったものを除去するものです。

代表的なものに、Proxomitron-JPrivoxy があります。

Proxomitron や Privoxy は、大変有用なソフトウェアですが、フィルタのルールを独自の言語で書かなければなりません。

「そんなのヤだ! ヤだ! Ruby じゃなきゃヤだ!」と言う声が聞こえてきますね。という か、そうでしょ。そうなんでしょ。そうだと言ってくれ。そうじゃないとこの記事は成り たたないのだっ!!

……というわけで、Ruby でコンテンツフィルタを書いてみましょう。

しかし、プロキシサーバを一から書くとなると、大変です。面倒です。やってられません。

そこで WEBrick ですよ!

WEBrick ってなに?

WEBrick っていうのは、知ってる人は知ってるし、知らない人は知らない、そんなライブ ラリです。

……と、こんなことで済ますと編集長に怒られるので、真面目に説明します。

RAA:webrick によると、

 WEBrick is a Ruby library program to build HTTP servers.

だそうです。日本語にすると、「WEBrick は HTTP サーバを作るためのライブラリだよっ ♪」ということですね。このライブラリを使えば HTTP サーバを数行で書けます。す げぇー。*1

しかも、Ruby 1.8 系標準添付なので、気軽に使えるのも良いのですよ。

WEBrick でプロキシサーバを作る

WEBrick ってのは、HTTP サーバだけじゃなくて、HTTP Proxy サーバを作るための機能 もついてるんですよ。WEBrick::HTTPProxyServer というクラスがそれ。 これを使えば、HTTP プロキシサーバも数行で書けます。

で、書いてみました。

 #!/usr/bin/env ruby

 require 'webrick'
 require 'webrick/httpproxy'

 # プロキシサーバオブジェクトを作る
 s = WEBrick::HTTPProxyServer.new({})

 # SIGINT を捕捉する。
 Signal.trap('INT') do
   # 捕捉した場合、シャットダウンする。
   s.shutdown
 end

 # サーバを起動する。
 s.start

うわぁ。本当に数行だよ。これでプロキシサーバとして動作するってんだから凄いよねぇ。

「書いた気がしない」とか「手抜きだ」とか言わないように。そのへんはまぁ、あれだよ。 うん。

君が自前主義症候群なのだよ

そうだ。そうに違いない。こんなに便利なものがあるんだから、それを使えば良いのだ。 自分はもっと高いレベルの部分を実装することに力を入れるのだ。車輪の再発明は避ける のだ!

ということで。

えぇーっと、一行ずつに解説とかは面倒なんでやめます。コメントを懇切丁寧に書いてあ るんで、それを読んでください。

さて、これを simpleproxy.rb とかいう名前で保存して、普通に ruby simpleproxy.rb とかやって起動すれば、ちゃんとプロキシサーバとして動きます。

もうちょっとオプションとか指定したい

前の章で作ったのは、

WEBrick だとこんなに短かくプロキシサーバを書けるんだぞ

というのを見せるためのプログラムなので、駄目駄目です。ポート番号が80番だったりし ます。

というわけで、ちゃんとオプションを設定しましょう。

 require 'uri'

 # プロキシサーバオブジェクトを作る
 s = WEBrick::HTTPProxyServer.new(
 # バインドアドレス(デフォルト:nil)
   :BindAddress => '127.0.0.1',
 # ポート番号(デフォルト:80)
   :Port => 8080,
 # ログ(デフォルト:nil)
   :Logger => WEBrick::Log::new("log.txt", WEBrick::Log::DEBUG),
 # proxy が通ってきたことを header で報せるか(デフォルト:true)
   :ProxyVia => false,
 # 親プロキシ(デフォルト:nil)
   :ProxyURI => URI.parse('http://localhost:3128/')
 )

さて、:Logger は、WEBrick::Log::new("log.txt", WEBrick::Log::DEBUG) を設定していますね。ここは重要なので、解説しておきます。

WEBrick::Log.new() の第一引数は、ログの出力先です。ファイル名を与えると、そのファ イルにログを吐きます。nil を与えると、$stderr にログを吐きます。それ以外なら、与 えられた引数に << します。

第二引数は、ログの出力レベルです。FATAL、ERROR、WARN、INFO、DEBUG の中から、用途 に応じて選んでください。今回はすべてのログを見たいので、WEBrick::Log::DEBUG を選 びました。

他にも色々ありますが、まぁこのぐらいを設定すれば十分でしょう。さらなるオプション は lib/webrick/config.rb を読んで調べてください。なぜソースを読めというのかって?

ドキュメントが無い

からだよっ!

ハンドラを使う

さて、これだけだと、ただ丸投げするだけの HTTP プロキシサーバです。今回作 りたいのはコンテンツフィルタですから、こっからが本番です。

で、コンテンツフィルタなんてものを作るための機能が WEBrick にあるのかなー?と 言うと……あるんですねー。その名もハンドラです。パンドラでもゴンドラでもあ りません。ハンドラです。こいつを使います。

ハンドラはこんな感じで書きます。

 handler = Proc.new() {|req,res|
   if req.unparsed_uri =~ %r!a.hatena.ne.jp! and res['content-type'] =~ %r!text/html!
     res.body.gsub!(%r=<!-- TG-Affiliate Banner Space -->.*<!-- /TG-Affiliate Banner Space -->=m, '')
   end
 }

Proc を使うんですね。で、この手続きを

 # プロキシサーバオブジェクトを作る
 s = WEBrick::HTTPProxyServer.new(
 # ハンドラ
   :ProxyContentHandler => handler
 )

こんな感じでプロキシサーバオブジェクトに設定してやれば、通信が行われた時に call されます。

さて、ハンドラの中身です。引数として、req と res というのが渡されてますね。req と res ってのは WEBrick::HTTPRequest と WEBrick::HTTPResponse のインスタンスです。

WEBrick::HTTResponse の方をイジれば、ダウンロードしてきたコンテンツを改変できま す。

ハンドラはデータのダウンロードが終了した後に呼ばれるので、リクエストをイジること は出来ません。WEBrick::HTTPRequest の方をイジっても無意味です。無視されます。そ して悲しい気分になれます。イジっても実害は無いので、悲しい気分になりたい方はイジっ て頂いても構いません。*2

この二つにはいっぱいインスタンス変数がついてます。それらをイジって、色々遊べるわ けです。

でまぁ、それっぽいインスタンス変数の一覧をあげてみました。大体名前でわかるでしょ。 うん。そゆことでよろしく。

詳しい中身については、次の章で説明します。

  • WEBrick::HTTRequest の役立ちそうなインスタンス変数
    • cookies
    • body
    • request_method
    • request_uri
  • WEBrick::HTTPResponse の役立ちそうなインスタンス変数
    • request_uri
    • host
    • port
    • path
    • query_string
    • header
    • cookies

実際にフィルタを書いてみよう

では、いよいよ実際にフィルタを書いてみましょう。

さて、どんなフィルタをかけるのか、というところが問題になってくるわけですが、 Proxomitron のフィルタでも、一番需要が多いのが、広告カッターです。ウザい広告を 取り除きたい、のですよ。そういうことで、今回は広告カッターを例題として取り上げま す。

さて、どこの広告をカットするか、ということが問題になってくるわけですが、今回は皆 大好き、スラシュドットジャパンの広告をカットしてみましょう。

まずは、http://slashdot.jp/ を開いて、ソースを表示します。すると、いかにも広告っ ぽい部分があります。

 <!-- begin ad code -->
 (中略)
 <!-- end ad code -->

という部分です。広告は英語で ad ですから、きっとここが広告です。

では、この部分をカットするハンドラを定義しましょう。

 handler = Proc.new() {|req,res|
   if req.host == 'slashdot.jp' and res['content-type'] =~ /text\/html/
     res.body.gsub!(/<!-- begin ad code -->.*?<!-- end ad code -->/m, '')
   end
 }

簡単そうなコードですね。順番に見ていきましょう。

さて、もう一度確認すると、req は WEBrick::HTTPRequest のインスタンス、res は WEBrick::HTTPResponse のインスタンスです。

req.host には、リクエストされたホストの名前が入っています。今回はslashdot.jp の 広告を抜きたいので、それにマッチするかを調べています。

res#[] には相手サーバの返してきた HTTP ヘッダが入っています。ここでは、' content-type' をキーとしたときに帰ってくるものが text/html であるかどうか、を調 べています。

まとめると、「slashdot.jp に対するリクエストで、コンテンツのタイプが html のもの」 の時、この条件は真になります。

さて、if の中身です。res.body には、向こうのサーバから帰ってきた本文が含まれてい ます。これは書換え可能データなので、容赦なく書換えてしまって構いません。今回は、 広告部分にマッチする部分をカットしています。

これまでの成果をまとめると、次のようになります。

 #!/usr/bin/env ruby

 require 'webrick'
 require 'webrick/httpproxy'
 require 'uri'

 handler = Proc.new() {|req,res|
   if req.host == 'slashdot.jp' and res['content-type'] =~ /text\/html/
     res.body.gsub!(/<!-- begin ad code -->.*?<!-- end ad code -->/m, '')
   end
 }

 # プロキシサーバオブジェクトを作る
 s = WEBrick::HTTPProxyServer.new(
 # バインドアドレス(デフォルト:nil)
   :BindAddress => '127.0.0.1',
 # ポート番号(デフォルト:80)
   :Port => 8080,
 # ログ(デフォルト:nil)
   :Logger => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG),
 # proxy が通ってきたことを header で報せるか(デフォルト:true)
   :ProxyVia => false,
 # 親プロキシ(デフォルト:nil)
   :ProxyURI => URI.parse('http://localhost:3128/'),
 # ハンドラ
   :ProxyContentHandler => handler
 )

 # 後処理の為に SIGINT を捕捉する。
 Signal.trap('INT') do
   # 捕捉した場合、シャットダウンする。
   s.shutdown
 end

 # サーバを起動する。
 s.start

これを、slashdotfilterproxy.rb とでも名前をつけて保存してください。そして、コマ ンドラインから、おもむろに

 $ ruby slashdotfilterproxy.rb

と打つと、

 [2004-09-12 11:05:04] INFO  WEBrick 1.3.1
 [2004-09-12 11:05:04] INFO  ruby 1.8.2 (2004-08-24) [i386-linux]
 [2004-09-12 11:05:04] DEBUG TCPServer.new(127.0.0.1, 8080)
 [2004-09-12 11:05:04] INFO  WEBrick::HTTPProxyServer#start: pid=2305 port=8080

と表示されて、サーバが起動します。

あとはブラウザのプロキシ設定を、自作のプロキシに向けるだけです。

http://slashdot.jp/ にアクセスすると、上部の広告がカットされている筈です。

宿題

 「。」を「にょ。」に変換するプロキシサーバを書きなさい。

解けた方は私にメールで送ってください。宛先は tokuhirom at yahoo dot co dot jp で す。

おわりに

この記事では、WEBrick を使って簡単なプロキシサーバの作り方を紹介しました。

フィルタを Ruby で自由に書けるので、可能性は無限大です。皆さんも自分でフィルタ を書いてみてください。

日常的に使うものなので、使っているうちに改良したい点が出てくると思います。Ruby が上達するコツは頻繁にプログラムを書くことですから、よい練習になると思います。

宿題にも挑戦してください。ポイントは文字コードの扱いです。UTF-8 なページを 「にょ。」な語尾に変換できない、なんてのは、ありえません

参考文献

著者について

tokuhirom.jpg

まつの とくひろ

自称、日本で一番モンキーレンチが似合う男。

日々TokuLog! で毒を吐くも、本当は「いい日記」を目指したいと思っている。

ご意見・要望・バグレポートは tokuhirom at yahoo dot co dot jp まで。

更新日時:2005/10/04 16:22:36
キーワード:
参照:[Rubyist Magazine 0002 号] [分野別目次] [各号目次]

*1 俺ってばスゲー感を手軽に得られます

*2 私は推奨いたしません