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
は指定した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::HTTP
はno_proxy
をサポートしていることがわかりました。
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
})
どうやらコネクションインスタンスの初期化で環境変数の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_proxy
やno_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
に設定される@proxy
に設定されるno_proxy
を考慮したコードになります。
利用側でも
# ...(省略)...
# 内部へのアクセスなのでプロキシ経由させたくない
conn = Faraday.new(:url => 'http://foo.internal/')
response = conn.get('/users') # => 直接接続!
と謎の空文字を使わなくて済みました。
あとは頑張って英語とかGoogle翻訳を駆使して本家にプルリクエストをおくりましょう。
Support no_proxy via URI::Generic#find_proxy
議論の結果、ちゃんと取り込まれたみたいですね!
今回は仕事中に困ったことからOSSへ実際にパッチを送るまでの流れをみました。 仕事でおやっ?と思ったことがあれば調べてみて、隙があればパッチを送ってみましょう。 そしてマージされたらどんどん自慢しましょう。ちょっとした自信につながるはずです。