NaClの中村です。

OSSに関するイベントの会場などでたまに「OSSにコードコントリビュートしたいんですけどなかなかできなくて…」と聞くことがあります。 私も含め、OSSのイベントに参加されるような方は単にOSSを利用するだけでなく、コードコントリビュートしたい人が多いようです。 確かに自分が書いたコードが広く普及しているOSSに入り、いろんなところで動くのは気持ちのよいものです。

しかし、我々プログラマは日頃のお仕事もあるし、家に帰れば猫の世話に追われ、見たいアニメもある。 ツイッターでは「すごーい!」とつぶやきたいし、とても多忙な日々を過ごしています。 パッチを書いている暇はないのです。

そこでお仕事の時間を利用してパッチを書いてみようというのが今回の趣旨です。 この記事では私が実際にお仕事をしつつパッチを書いた例を見つつ、どのようにOSSにコードコントリビュートするのか紹介したいと思います。

動かないコードの発見

お仕事で以下のような状況がありました。

プロキシ環境

外部にでるときはプロキシ経由を利用してHTTPアクセスし、内部へのHTTPアクセスはプロキシを経由せずに直接つなぎに行きます。 HTTPアクセスにはfaradayというライブラリを使っており、イメージとしては以下のようなコードを動かしていました。

require 'faraday'

# プロキシサーバを指定
ENV['http_proxy'] = "http://proxy-server/"
# プロキシ経由しないhostを指定
ENV['no_proxy'] = "foo.internal"

# 外にでるためプロキシ経由
conn = Faraday.new(:url => 'http://www.example.com/')
response = conn.get('/users')

# 内部へのアクセスなのでプロキシ経由させたくない
conn = Faraday.new(:url => 'http://foo.internal/')
response = conn.get('/users') # => なぜかプロキシを経由してしまう!!

ところが実際に動かしてみると環境変数のno_proxyがうまく動いていないことがわかりました。

環境変数のno_proxy

ここで仕事の時間を利用してno_proxyについて調査をはじめます。

環境変数のno_proxyは指定したIPアドレスに対しプロキシ経由させない設定のようです。 またno_proxyの対応状況はHTTPクライアントによって異なるみたいです。 ということで 今回はRubyの対応状況について調べてみましょう。

Ruby2.0より前のNet::HTTPでは以下のように明示的にプロキシを設定する方法しかありませんでした。

require 'net/http'
http = Net::HTTP::Proxy('proxy.example.com', 8080).new('www.example.com')
http.get('/ja') # proxy.example.com 経由で接続

Ruby2.0からFeature #6546が取り込まれ、明示的にプロキシを指定しない場合にはそれぞれの環境変数を見てくれるようになりました。

require 'net/http'

# プロキシサーバを指定
ENV['http_proxy'] = "http://proxy.example.com:8080/"
ENV['no_proxy'] = "www.example.org"

http = Net::HTTP.new('www.example.com')
http.get('/users') # プロキシ経由

http = Net::HTTP.new('www.example.org')
http.get('/ja') # 直接接続

調べてみた結果、Net::HTTPno_proxyをサポートしていることがわかりました。

faradayの問題

faradayは現存するRubyのHTTPクライアントに共通のインターフェースを設けるラッパーです。 faradayを使っていさえいれば中身にNet::HTTPを使ってもいいし、途中でHTTPClient乗り換えてもいいというものです。

はじめに述べた問題のコードではfaradayを経由してNet::HTTPを利用していました。 Net::HTTPではno_proxyをサポートしているので、どうもfaradayが何かしていそうです。

ここでfaradayのコードを眺めに行きます。

# lib/faraday/connection.rb:82
      @proxy = nil
      proxy(options.fetch(:proxy) {
        uri = ENV['http_proxy']
        if uri && !uri.empty?
          uri = 'http://' + uri if uri !~ /^http/i
          uri
        end
      })

lib/faraday/connection.rb:82

どうやらコネクションインスタンスの初期化で環境変数のhttp_proxyを見て@proxyに設定するみたいです。

次にNet::HTTPとのアダプター部分を見てみましょう。

# lib/faraday/adapter/net_http.rb:88
      def net_http_connection(env)
        if proxy = env[:request][:proxy]
          Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:user], proxy[:password])
        else
          Net::HTTP
        end.new(env[:url].host, env[:url].port || (env[:url].scheme == 'https' ? 443 : 80))
      end

lib/faraday/adapter/net_http.rb:88

env[:request][:proxy]には環境変数のhttp_proxyが入ります。 コードを読むとhttp_proxyを指定した時は明示的なプロキシの指定(Net::HTTP::Proxy)をしているようです。 上記の明示的な指定にはno_proxyの考慮がないため、うまく動いていなかったようです。

利用側で回避できる問題なのか

ここまでライブラリを調べたあと、私の場合は利用者側で回避できないか検討します。 というのはライブラリ側としてはこれが実は正しい挙動で、別の方法が利用者側に提供されているかもしれないからです。

faradayの利用側で、no_proxy見てくれない問題を回避する方法は以下のとおりです。

# ...(省略)...

# 内部へのアクセスなのでプロキシ経由させたくない
conn = Faraday.new(:url => 'http://foo.internal/', :proxy => '')
response = conn.get('/users') # => 直接接続!

なんやかんやあって引数のproxyに空文字列いれたらよさそうですが…。 このコードをパッと見てもなんでこうなるのかわからないし、これはライブラリ側を直したほうがよさそうという結論にいたります。

パッチを書く

ここでfaradayをどういう風に直したらいいのか考えます。

faradayは様々なHTTPクライアントのラッパーです。 環境変数http_proxyno_proxyの扱いは各HTTPクライアントでまちまちですが、ここの差分も吸収したいのでしょう。 ですが、faradayはその扱いが雑でno_proxyの考慮ができていませんでした。

この辺りを独自で実装するのは面倒ですので色々と調べてみます。 そうするとRubyにURI::Generic#find_proxyという便利なメソッドを見つけました。 これをうまくfaradayに組み込めばうまく行きそうですね。

ということでパッチを書いてみます。

       @proxy = nil
        proxy(options.fetch(:proxy) {
 -        uri = ENV['http_proxy']
 -        if uri && !uri.empty?
 -          uri = 'http://' + uri if uri !~ /^http/i
 -          uri
 +        URI.parse(url).find_proxy
        })

実際には互換性なども考慮してもう少し複雑ですが、大体こんな感じです。 接続先のurlに対してfind_proxyを呼んでやることで以下のように

  • 接続先がno_proxyの対象ならnil@proxyに設定される
  • 接続先がそれ以外ならプロキシサーバのURIが@proxyに設定される

no_proxyを考慮したコードになります。

利用側でも

# ...(省略)...

# 内部へのアクセスなのでプロキシ経由させたくない
conn = Faraday.new(:url => 'http://foo.internal/')
response = conn.get('/users') # => 直接接続!

と謎の空文字を使わなくて済みました。

あとは頑張って英語とかGoogle翻訳を駆使して本家にプルリクエストをおくりましょう。

Support no_proxy via URI::Generic#find_proxy

議論の結果、ちゃんと取り込まれたみたいですね!

まとめ

今回は仕事中に困ったことからOSSへ実際にパッチを送るまでの流れをみました。 仕事でおやっ?と思ったことがあれば調べてみて、隙があればパッチを送ってみましょう。 そしてマージされたらどんどん自慢しましょう。ちょっとした自信につながるはずです。