Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14565] ファイルの概要

このコミットは、Go言語のnet/httpパッケージにおけるhttp_proxy環境変数のパースに関するバグ修正です。具体的には、http_proxyの値がプロトコル(http://など)を含まない形式で指定された場合に、url.Parseがホスト名をスキームとして誤認識し、プロキシ設定が正しく適用されない問題を解決します。

コミット

commit a034fc9855b307ab5e9e5da04602823d6414f512
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Dec 5 19:08:42 2012 -0800

    net/http: fix bug parsing http_proxy lacking a protocol
    
    Per the curl man page, the http_proxy configuration can be
    of the form:
    
       [protocol://]<host>[:port]
    
    And we had a test that <ip>:<port> worked, but if
    the host began with a letter, url.Parse parsed the hostname
    as the scheme instead, confusing ProxyFromEnvironment.
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/6875060

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a034fc9855b307ab5e9e5da04602823d6414f512

元コミット内容

net/http: プロトコルを持たないhttp_proxyのパースバグを修正。 curlのmanページによると、http_proxyの設定は[protocol://]<host>[:port]の形式を取り得ます。 既存のテストでは<ip>:<port>形式が機能することを確認していましたが、ホスト名が文字で始まる場合、url.Parseがホスト名をスキームとしてパースしてしまい、ProxyFromEnvironmentを混乱させていました。

変更の背景

この変更の背景には、Goのnet/httpパッケージが、http_proxy環境変数の解釈において、curlなどの一般的なツールが許容する形式の一部を正しく扱えていなかったという問題があります。

http_proxy環境変数は、HTTPリクエストをプロキシサーバー経由で送信するために広く利用されています。curlのmanページに記載されているように、この変数の値は[protocol://]<host>[:port]という形式を取ることができます。ここで重要なのは、protocol://の部分がオプションであるという点です。例えば、cache.corp.example.com:1234のように、プロトコルを明示せずにホスト名とポート番号のみを指定する形式も有効とされています。

しかし、Goのnet/httpパッケージ内のProxyFromEnvironment関数が内部で利用しているurl.Parse関数は、このプロトコルが省略された形式を正しく解釈できないケースがありました。具体的には、127.0.0.1:8080のようなIPアドレスで始まるホスト名の場合は問題なくパースできていましたが、cache.corp.example.com:1234のようにホスト名がアルファベットで始まる場合url.Parsecache.corp.example.com全体をURLのスキーム(プロトコル部分)として誤って認識してしまっていました。

これにより、ProxyFromEnvironment関数は、プロキシURLのスキームが空であると判断し、プロキシ設定を適用できないというバグが発生していました。このバグは、ユーザーが環境変数でプロキシを設定しているにもかかわらず、Goアプリケーションがプロキシを使用しない、または誤ったプロキシ設定で動作するという問題を引き起こしていました。

このコミットは、このurl.Parseの誤解釈を修正し、http_proxy環境変数がプロトコルを省略した形式でも正しく機能するようにすることを目的としています。

前提知識の解説

http_proxy環境変数

http_proxyは、多くのUnix系システムやWindowsで利用される環境変数で、HTTPリクエストを送信する際に使用するプロキシサーバーのアドレスを指定するために使われます。通常、http_proxy=http://proxy.example.com:8080のように設定されますが、本コミットの背景にあるように、proxy.example.com:8080のようにプロトコルを省略した形式も一部のツール(curlなど)では許容されます。

Go言語のnet/urlパッケージとurl.Parse関数

Go言語の標準ライブラリであるnet/urlパッケージは、URLのパース、生成、クエリパラメータの操作など、URLに関する機能を提供します。 url.Parse(rawurl string)関数は、与えられた文字列をURLとしてパースし、*url.URL構造体を返します。この構造体には、スキーム(http, httpsなど)、ホスト、パス、クエリなどのURLの各要素が格納されます。

url.Parseの挙動の特性として、入力文字列に://が含まれない場合、最初の:までの部分をスキームとして解釈しようとします。例えば、http://example.comhttpをスキームとして認識します。しかし、example.com:8080のような文字列の場合、example.comをスキームとして誤って解釈してしまうことがあります。これは、URLのスキームがアルファベットで始まるという一般的なルールに合致するためです。

Go言語のnet/httpパッケージとProxyFromEnvironment関数

Go言語のnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。 http.ProxyFromEnvironment(req *Request)関数は、環境変数(HTTP_PROXY, HTTPS_PROXY, NO_PROXYなど)に基づいて、指定されたリクエストに対するプロキシURLを決定します。この関数は、内部で環境変数の値を読み取り、それをurl.ParseでパースしてプロキシURLを構築します。

本コミットの修正前は、ProxyFromEnvironment関数内でurl.Parseの結果をチェックする際に、proxyURL.Scheme == ""という条件を使用していました。これは、プロキシURLにスキームが全く含まれていない場合に、http://を補完して再パースするというロジックでした。しかし、前述のurl.Parseの誤解釈により、cache.corp.example.com:1234のようなケースではcache.corp.example.comがスキームとして認識されてしまい、proxyURL.Schemeが空ではなくなるため、http://の補完ロジックが発動せず、結果としてプロキシ設定が正しく適用されない問題が発生していました。

技術的詳細

このバグは、net/http/transport.go内のProxyFromEnvironment関数におけるurl.Parseの戻り値の解釈方法に起因していました。

元のコードでは、http_proxy環境変数の値(例: cache.corp.example.com:1234)をurl.Parseでパースした後、以下の条件でプロキシURLのスキームをチェックしていました。

if err != nil || proxyURL.Scheme == "" {
    if u, err := url.Parse("http://" + proxy); err == nil {
        proxyURL = u
        err = nil
    }
}

このロジックは、「パースエラーが発生した」または「パースされたURLのスキームが空である」場合に、http://をプレフィックスとして追加して再パースするというものでした。

問題は、cache.corp.example.com:1234のような文字列をurl.Parseが処理する際に発生しました。url.Parseは、://がない場合、最初のコロンまでの部分をスキームとして解釈しようとします。この場合、cache.corp.example.comがスキームとして認識されてしまいます。これは、cache.corp.example.comが有効なスキーム名(アルファベットで始まる)の形式に合致するためです。

その結果、proxyURL.Schemecache.corp.example.comとなり、空ではありません。したがって、上記のif条件のproxyURL.Scheme == ""の部分がfalseとなり、http://を補完して再パースするロジックが実行されませんでした。これにより、proxyURLcache.corp.example.com:1234という不正なスキームを持つURLとして扱われ、プロキシとして機能しなくなっていました。

修正は、このスキームのチェック方法を変更することによって行われました。新しい条件は以下の通りです。

if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
    // ... (http://を補完するロジック)
}

この変更により、url.Parseが返したスキームがhttpで始まらない場合(例えば、cache.corp.example.comのようにホスト名がスキームとして誤認識された場合)、http://を補完して再パースするロジックが実行されるようになります。これにより、cache.corp.example.com:1234のような形式のhttp_proxyも正しくhttp://cache.corp.example.com:1234として解釈され、プロキシとして機能するようになります。

また、この修正を検証するために、transport_test.goに新しいテストケースが追加されました。これにより、プロトコルが省略されたホスト名形式のhttp_proxyが正しくパースされることが保証されます。

コアとなるコードの変更箇所

diff --git a/src/pkg/net/http/transport.go b/src/pkg/net/http/transport.go
index 48f7ac0e53..068c50ff0c 100644
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -90,7 +90,7 @@ func ProxyFromEnvironment(req *Request) (*url.URL, error) {
 		return nil, nil
 	}
 	proxyURL, err := url.Parse(proxy)
-	if err != nil || proxyURL.Scheme == "" {
+	if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
 		if u, err := url.Parse("http://" + proxy); err == nil {
 			proxyURL = u
 			err = nil
diff --git a/src/pkg/net/http/transport_test.go b/src/pkg/net/http/transport_test.go
index e49f14fa58..0e6cf85281 100644
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1068,6 +1068,9 @@ var proxyFromEnvTests = []struct {
 	wanterr error
 }{
 	{"127.0.0.1:8080", "http://127.0.0.1:8080", nil},
+	{"cache.corp.example.com:1234", "http://cache.corp.example.com:1234", nil},
+	{"cache.corp.example.com", "http://cache.corp.example.com", nil},
+	{"https://cache.corp.example.com", "https://cache.corp.example.com", nil},
 	{"http://127.0.0.1:8080", "http://127.0.0.1:8080", nil},
 	{"https://127.0.0.1:8080", "https://127.0.0.1:8080", nil},
 	{"", "<nil>", nil},

コアとなるコードの解説

src/pkg/net/http/transport.go

変更の中心は、ProxyFromEnvironment関数内のプロキシURLのスキームチェックロジックです。

  • 変更前:

    if err != nil || proxyURL.Scheme == "" {
    

    この条件は、「url.Parseでエラーが発生した」または「パースされたURLのスキームが空文字列である」場合に、http://を補完して再パースする処理を実行していました。しかし、前述の通り、cache.corp.example.com:1234のようなケースではproxyURL.Schemecache.corp.example.comとなり、空ではないため、この条件がfalseとなり、補完処理がスキップされていました。

  • 変更後:

    if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
    

    この新しい条件は、「url.Parseでエラーが発生した」または「パースされたURLのスキームがhttpで始まらない」場合に、http://を補完して再パースする処理を実行します。 strings.HasPrefix(proxyURL.Scheme, "http")は、proxyURL.Schemehttpまたはhttpsで始まるかどうかをチェックします。 もしproxyURL.Schemecache.corp.example.comのようにhttpで始まらない場合、!strings.HasPrefix(...)trueとなり、http://を補完するロジックが実行されます。これにより、cache.corp.example.com:1234は正しくhttp://cache.corp.example.com:1234として解釈されるようになります。

src/pkg/net/http/transport_test.go

このファイルには、ProxyFromEnvironment関数の動作を検証するためのテストケースが定義されています。今回の修正に合わせて、以下の3つの新しいテストケースがproxyFromEnvTestsスライスに追加されました。

  • {"cache.corp.example.com:1234", "http://cache.corp.example.com:1234", nil},
    • これは、プロトコルが省略され、ホスト名がアルファベットで始まる形式のプロキシ設定が、正しくhttp://が補完されてパースされることをテストします。
  • {"cache.corp.example.com", "http://cache.corp.example.com", nil},
    • これは、プロトコルとポート番号が省略され、ホスト名のみが指定された形式のプロキシ設定が、正しくhttp://が補完されてパースされることをテストします。
  • {"https://cache.corp.example.com", "https://cache.corp.example.com", nil},
    • これは、明示的にhttps://プロトコルが指定されたプロキシ設定が、正しくパースされることを確認するためのテストです。これは既存の動作の確認ですが、新しいロジックが既存の正しい動作を壊さないことを保証します。

これらのテストケースの追加により、http_proxy環境変数のパースロジックが、curlのmanページで定義されているような多様な形式を正しく扱えるようになったことが検証されます。

関連リンク

参考にした情報源リンク