ssh、socks、proxy.pac、spidermonkey、johnson

特定のホストからのみ接続できるwebサイトにアクセスしなければならない。しかし自ホストからの直接アクセスはできない。

こうしたとき、まず考えるのはproxyの利用だ。目的のwebサイトにアクセス可能な別のホスト(中間ホスト)でproxyサーバを動作させる。そしてそのproxyを経由して目的のwebサイトにアクセスする。この方法でめんどうなのは、アクセス元(自ホスト)、アクセス先(目的のwebサイト)とは本来関係のないホストで管理・運用作業が発生する点である。今後ずっと、というのでなければ採用しづらい。

次に考えるのはSSHによるポートフォワードを使った方法だ。こうした一時的な状況では、関係各ホストにSSHでならアクセスできることが多い。SSHの機能を使い、自ホストと中間ホストにトンネルを作る。webサイトへのアクセスを、このトンネルへのアクセスに代えることにより関接的なアクセスが可能となる。

ssh -L 20080:www.example.com:80 proxy.exmaple.jp

この例では自ホストの20080ポートへのアクセスが中間ホスト(proxy.example.jp)を経由して目的のwebサイト(www.example.comの80ポート)へとつながっていく。中間ホストで特別な作業は発生せず、手軽でよいのだが、一般的なブラウザでアクセスする際に「http://www.example.jp」でなはく「http://localhost:20080」を指定しなければらない問題がある。単純なところではリンク切れが起きる可能性がある。

通常通りのURLを指定して、かつ、SSHのトンネルにアクセスを導くには、SSHおよびブラウザのSOCKS対応機能を使う。

sshにはSOCKSサーバとして動作する機能がある。また、多くのブラウザは、proxy機能の一部としてSOCKSクライアントとしての動作ができるようになっている。proxy機能であるから、通常通りのURLを指定しても実際のアクセスはSOCKSサーバに振り向けられる。よってURLを「localhost」などと変えなくてもすむ。

ssh -D 21080 proxy.example.jp

ブラウザではSOCKSサーバをlocalhost、ポートを21080とする。この設定したブラウザは、すべての通信を指定のSOCKSサーバを介して行うようになる。となると、目的のwebサイト意外へのアクセスまで含めて中間サイトからアクセスする状況に陥いってしまう。これは通常やりすぎである。

SOCKSサーバの使用をアクセス先によって選択することができないか。FirefoxならFoxyProxy、ChromeならProxy Switchy。これらはまさにそのための拡張だ。ところが、現在のProxy Switchyには問題があるようで、こうした動的なproxy選択の機能が動作しない。この問題の回避方法もあるようなのだが、そのためにはシステムのproxy設定を変更することになる(そもそもそうした動作をするものであるようだ)。そして、そのための設定にはPACと呼ばれるファイルを用いる。

PACには、ブラウザがproxyの使用が必要かどうかを判定するための要件を記述できる。これまでほとんど縁がなかったため考えから抜け落ちていたのだが、前述した拡張を使うまでもなく、適切な内容でPACを作成してシステムに適用すればよい。

PACの書き方については検索すればいくつもの情報が見つかる。けれども、それらの多くで参照している大本のnetscapeのサイトが消失しておりなかなか追求できない。そのため現在ではWikipediaを参照するのがよさそうだ。日本語ではプロキシの自動設定方法PacファイルによるProxyの指定などを併わせて読むと参考になる。今回のケースでは次のような記述をすれば十分であろう。

function FindProxyForURL(url, host) {
  if (shExpMatch(url, '*://www.example.jp/*')) {
    return 'SOCKS localhost:21080';
  }

  return 'DIRECT';
}

この程度の簡単な内容であれば動作確認は簡単である、と言いたいところだが、実際のところ「()」の書き忘れをするなどということをしてしまい書き換えと確認を何度か繰り返し行った。そこで、w3cのHTML validatorのようなものはないものかと探してみたのだが見付からない。次に、結局のところPACはJavaScriptであるためコマンドラインから実行するツールを探してみたところpactesterが見付かった。

pactesterはJavaScriptの実装であるSpiderMonkeyと、PAC記述のためのサポートスクリプト(JavaScript)およびコマンドそのものとなるPerlスクリプトからなる。SpiderMonkeyとPerlを接続するためにJavaScript::SpiderMonkeyというライブラリを使っている。問題はこのライブラリのパッケージが手元の環境にはない点である。もちろんCPANを使うなどすればよいのだろうが……。

別の実装はないかと探してみたがこれというものを見付けられない。SpiderMonkeyのほうで何かないかと検索範囲を広げていったところ、JavaScriptのテストというテーマの中で発表されたさいきんのJavaScriptテストの資料に行きあたった。同資料の中ではAjaxを含む、というよりそれをメインのターゲットにしたJavaScriptのテストを支えるテスティングフレームワークの興味深い内容に触れられている。そんな中に、今回の状況、PACのテストに使えそうなものがあった。

JohnsonはRubyとSpiderMonkeyを結び付けるライブラリである。前述のJavaScript::SpiderMonkeyがPerlとJavaScriptの中間に立ったように、このライブラリはRubyとSpiderMonkeyの中間に立つ。資料の本題ではEnvjsと組み合わさることでブラウザ上でのJavaScriptコードの動きをコマンドライン上で検証できる様子が示されている。もちろん、ここではそこまで高度なことは必要ない。PACを実行し、その動作が想定通りであることを確認するのが目的である。

前掲のPACのような簡単なものでもJavaScriptの組み込みではない関数が登場する。一般的なJavaScript実装においてPACの動作を検証するためには、これらの関数を何らかの形で実装しなければならない。だが、これを独自に実装するのでは本末転倒である。幸い、先に紹介したpactesterにそのためのJavaScriptコードが含まれている。pac_utils.jsがそのコードであり、もともとはmozillaのコードの一部をもとにしたものだそうだ。

pac_utils.jsがあれば十分かというと、もう少し足りない。Wikipediaの解説にもある通り、さらに二つの関数、dnsResolve()とmyIpAddress()を実行系で使用できるようにする必要がある。pactesterではこの部分をPerlコードとして実装しており、Johnsonを使うのであればRubyコードで実装することになる。このようにしてできたのが以下のPACテスト用スクリプトだ。

pac_file = 'proxy.pac'

tests = [
  # {:url => 'http://example.jp', :expect => 'proxy spec', ... }, ...
  {
    :url => 'http://www.example.jp/',
    :expect => 'SOCKS localhost:21080',
  },
  ...
]

hosts = {
  # hostname => '10.1.1.1', ...
}

require 'uri'
require 'socket'
require 'rubygems'
require 'johnson'

def dns_resolve(host)
  Socket.gethostbyname(host)[3].unpack('C4').join('.')
rescue
  nil
end

runtime = Johnson::Runtime.new
runtime.load('pac_utils.js')
runtime.load(pac_file)
runtime['dnsResolve'] = proc do |host|
  map[host] || dns_resolve(host)
end

failed = false
tests.each do |hash|
  runtime['url'] = hash[:url]
  runtime['host'] = hash[:host] || URI(hash[:url]).host
  runtime['myIpAddress'] = hash[:client_ip] || '10.0.0.1'

  expect = hash.delete(:expect)
  proxy = runtime.evaluate('FindProxyForURL(url, host)')
  unless expect == proxy
    puts "failed: #{hash.inspect} #=> #{proxy} (expected #{expect})"
    failed = true
  end
end

exit 1 if failed