[インデックス 15683] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http/httputil
パッケージ内の ReverseProxy
において、HTTPの「ホップバイホップヘッダー」を適切に削除する変更を導入しています。これにより、リバースプロキシがバックエンドサーバーにリクエストを転送する際に、これらのヘッダーが誤って転送されることを防ぎ、HTTPプロトコルの仕様に準拠した動作を保証します。
コミット
commit c6e8993e79653402b2aa1c3546920d0020b6d032
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 11 10:32:32 2013 -0700
net/http/httputil: remove hop-by-hop headers in ReverseProxy
Fixes #2735
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7470048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c6e8993e79653402b2aa1c3546920d0020b6d032
元コミット内容
このコミットは、net/http/httputil
パッケージの ReverseProxy
が、クライアントから受け取ったHTTPリクエストをバックエンドサーバーに転送する際に、HTTP/1.1の仕様で定義されている「ホップバイホップヘッダー」を削除するように修正します。特に Connection
ヘッダーは、リバースプロキシとバックエンド間の永続的な接続を維持するために重要であり、クライアントからの値がそのまま転送されると問題が発生する可能性があります。この変更は、GoのIssue #2735 を解決します。
変更の背景
HTTP/1.1プロトコルでは、ヘッダーは大きく分けて「エンドツーエンドヘッダー」と「ホップバイホップヘッダー」の2種類に分類されます。
- エンドツーエンドヘッダー: リクエストの送信元から最終的な宛先まで、メッセージ全体に適用されるヘッダーです。例えば
Content-Type
,Content-Length
,Authorization
などがあります。これらはプロキシを介しても変更されずに転送されるべきです。 - ホップバイホップヘッダー: 単一のTCP接続にのみ適用され、その接続を越えて転送されるべきではないヘッダーです。これらはプロキシやゲートウェイによって処理され、次のホップに転送される前に削除される必要があります。RFC 2616のセクション13.5.1で明確に定義されています。
ReverseProxy
は、クライアントからのリクエストを受け取り、それを別のサーバー(バックエンド)に転送する役割を担います。この際、クライアントとリバースプロキシ間の接続と、リバースプロキシとバックエンド間の接続は、それぞれ独立したHTTP接続です。もしホップバイホップヘッダーがクライアントからリバースプロキシ、そしてバックエンドへとそのまま転送されてしまうと、バックエンドサーバーが誤った解釈をする可能性があります。
特に Connection
ヘッダーは、現在の接続でどのヘッダーがホップバイホップであるかを示すために使用されます。例えば、クライアントが Connection: close
を送ってきた場合、それはクライアントとリバースプロキシ間の接続を閉じることを意味しますが、リバースプロキシとバックエンド間の接続には影響しません。しかし、もしこの Connection: close
がバックエンドに転送されてしまうと、バックエンドはリバースプロキシとの接続を閉じてしまい、リバースプロキシが永続的な接続(Keep-Alive)を望んでいてもそれが実現できなくなります。
このコミットは、このようなプロトコル違反や予期せぬ動作を防ぐために、ReverseProxy
がホップバイホップヘッダーを適切に処理し、バックエンドへのリクエストから削除するように修正することを目的としています。
前提知識の解説
HTTP/1.1 プロトコルとヘッダー
HTTP (Hypertext Transfer Protocol) は、Web上でデータを交換するためのプロトコルです。HTTP/1.1は、現在でも広く利用されているバージョンであり、その仕様はRFC 2616で定義されています。HTTPメッセージは、リクエストライン/ステータスライン、ヘッダー、そしてボディで構成されます。ヘッダーは、メッセージに関するメタデータを提供し、クライアントとサーバー間の通信を制御します。
ホップバイホップヘッダー (Hop-by-hop Headers)
RFC 2616のセクション13.5.1「Connection」ヘッダーフィールドの定義では、ホップバイホップヘッダーについて以下のように述べられています。
HTTP/1.1プロキシは、メッセージを転送する前に、受信したメッセージからすべてのホップバイホップヘッダーフィールドを削除しなければならない。
RFC 2616で定義されているホップバイホップヘッダーは以下の通りです。
Connection
Keep-Alive
Proxy-Authenticate
Proxy-Authorization
Te
(Transfer-Encodingの別名、または拡張)Trailers
Transfer-Encoding
Upgrade
これらのヘッダーは、単一のTCP接続の特性や、その接続におけるメッセージの転送方法を制御するために使用されます。そのため、プロキシを介して複数の接続が存在する場合、これらのヘッダーは各接続の境界で処理され、次の接続には転送されるべきではありません。
リバースプロキシ (Reverse Proxy)
リバースプロキシは、クライアントからのリクエストを受け取り、それを一つまたは複数のバックエンドサーバーに転送し、バックエンドからのレスポンスをクライアントに返すサーバーです。クライアントはリバースプロキシと通信していると認識しますが、実際にはリバースプロキシがバックエンドサーバーとの通信を仲介しています。
リバースプロキシの主な用途は以下の通りです。
- 負荷分散 (Load Balancing): 複数のバックエンドサーバーにリクエストを分散し、サーバーの負荷を均等にする。
- セキュリティ (Security): バックエンドサーバーのIPアドレスを隠蔽し、直接的な攻撃から保護する。SSL/TLS終端を行うことで、バックエンドサーバーの負荷を軽減する。
- キャッシュ (Caching): 静的コンテンツをキャッシュし、バックエンドサーバーへのリクエスト数を減らす。
- URL書き換え (URL Rewriting): クライアントに見せるURLと、バックエンドサーバーが処理するURLを異なるものにする。
- A/Bテスト: 特定のユーザーグループを異なるバージョンのアプリケーションにルーティングする。
リバースプロキシは、HTTP通信の途中に位置するため、HTTPヘッダーの適切な処理が非常に重要になります。
技術的詳細
このコミットの技術的詳細は、net/http/httputil.ReverseProxy
がバックエンドへのリクエストを構築する際に、ホップバイホップヘッダーを明示的に削除するロジックを追加した点にあります。
以前のバージョンでは、Connection
ヘッダーのみが特別に処理され、削除されていました。しかし、RFC 2616で定義されている他のホップバイホップヘッダー(Keep-Alive
, Proxy-Authenticate
, Proxy-Authorization
, Te
, Trailers
, Transfer-Encoding
, Upgrade
)は削除されずにバックエンドに転送される可能性がありました。これはプロトコル違反であり、バックエンドサーバーでの予期せぬ動作やセキュリティ上の問題を引き起こす可能性がありました。
このコミットでは、以下の変更が行われました。
hopHeaders
変数の導入: RFC 2616で定義されているすべてのホップバイホップヘッダー名を格納する文字列スライスhopHeaders
が追加されました。これにより、削除すべきヘッダーのリストが一箇所に集約され、コードの可読性と保守性が向上しました。- 汎用的なヘッダー削除ロジック:
ServeHTTP
メソッド内で、outreq.Header
からhopHeaders
リストに含まれるすべてのヘッダーをループで削除するロジックが導入されました。 - ヘッダーマップのコピー最適化: ヘッダーを削除する際に、元のリクエストヘッダーマップがシャローコピーされている可能性があるため、実際にヘッダーを削除する必要がある場合にのみ、新しいヘッダーマップを作成し、既存のヘッダーをコピーする最適化が施されています。これにより、不要なメモリ割り当てとコピーを避けることができます。
copiedHeaders
というブール変数を使って、一度でもヘッダーの削除が必要になった場合にのみコピーが行われるようになっています。
この変更により、ReverseProxy
はHTTP/1.1の仕様に厳密に準拠し、より堅牢で予測可能な動作をするようになりました。
コアとなるコードの変更箇所
src/pkg/net/http/httputil/reverseproxy.go
--- a/src/pkg/net/http/httputil/reverseproxy.go
+++ b/src/pkg/net/http/httputil/reverseproxy.go
@@ -81,6 +81,19 @@ func copyHeader(dst, src http.Header) {
}
}
+// Hop-by-hop headers. These are removed when sent to the backend.
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
+var hopHeaders = []string{
+ "Connection",
+ "Keep-Alive",
+ "Proxy-Authenticate",
+ "Proxy-Authorization",
+ "Te", // canonicalized version of "TE"
+ "Trailers",
+ "Transfer-Encoding",
+ "Upgrade",
+}
+
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
transport := p.Transport
if transport == nil {
@@ -96,14 +109,21 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
outreq.ProtoMinor = 1
outreq.Close = false
- // Remove the connection header to the backend. We want a
- // persistent connection, regardless of what the client sent
- // to us. This is modifying the same underlying map from req
- // (shallow copied above) so we only copy it if necessary.\n-\tif outreq.Header.Get(\"Connection\") != \"\" {\n-\t\toutreq.Header = make(http.Header)\n-\t\tcopyHeader(outreq.Header, req.Header)\n-\t\toutreq.Header.Del(\"Connection\")
+ // Remove hop-by-hop headers to the backend. Especially
+ // important is "Connection" because we want a persistent
+ // connection, regardless of what the client sent to us. This
+ // is modifying the same underlying map from req (shallow
+ // copied above) so we only copy it if necessary.
+ copiedHeaders := false
+ for _, h := range hopHeaders {
+ if outreq.Header.Get(h) != "" {
+ if !copiedHeaders {
+ outreq.Header = make(http.Header)
+ copyHeader(outreq.Header, req.Header)
+ copiedHeaders = true
+ }
+ outreq.Header.Del(h)
+ }
}
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
src/pkg/net/http/httputil/reverseproxy_test.go
--- a/src/pkg/net/http/httputil/reverseproxy_test.go
+++ b/src/pkg/net/http/httputil/reverseproxy_test.go
@@ -29,6 +29,9 @@ func TestReverseProxy(t *testing.T) {
if c := r.Header.Get("Connection"); c != "" {
t.Errorf("handler got Connection header value %q", c)
}
+ if c := r.Header.Get("Upgrade"); c != "" {
+ t.Errorf("handler got Keep-Alive header value %q", c)
+ }
if g, e := r.Host, "some-name"; g != e {
t.Errorf("backend got Host header %q, want %q", g, e)
}
@@ -49,6 +52,7 @@ func TestReverseProxy(t *testing.T) {\n getReq, _ := http.NewRequest("GET", frontend.URL, nil)\n getReq.Host = "some-name"\n getReq.Header.Set("Connection", "close")\n+\tgetReq.Header.Set("Upgrade", "foo")\n \tgetReq.Close = true\n \tres, err := http.DefaultClient.Do(getReq)\n \tif err != nil {\n```
## コアとなるコードの解説
### `reverseproxy.go` の変更点
1. **`hopHeaders` グローバル変数の追加**:
```go
var hopHeaders = []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te", // canonicalized version of "TE"
"Trailers",
"Transfer-Encoding",
"Upgrade",
}
```
このスライスは、RFC 2616で定義されているすべてのホップバイホップヘッダー名を列挙しています。`Te` は `TE` の正規化された形式であり、HTTPヘッダー名は大文字小文字を区別しないため、Goの `http.Header` マップは正規化されたキーを使用します。
2. **`ServeHTTP` メソッド内のヘッダー削除ロジックの変更**:
以前は `Connection` ヘッダーのみを特別に処理していましたが、新しいコードでは `hopHeaders` リストをイテレートし、各ホップバイホップヘッダーが存在するかどうかを確認します。
```go
copiedHeaders := false
for _, h := range hopHeaders {
if outreq.Header.Get(h) != "" {
if !copiedHeaders {
outreq.Header = make(http.Header)
copyHeader(outreq.Header, req.Header)
copiedHeaders = true
}
outreq.Header.Del(h)
}
}
```
* `copiedHeaders` は、`outreq.Header` が元の `req.Header` のシャローコピーである可能性があるため、ヘッダーマップを実際にコピーしたかどうかを追跡するためのフラグです。
* `for _, h := range hopHeaders` ループは、定義されたすべてのホップバイホップヘッダーについて処理を行います。
* `if outreq.Header.Get(h) != ""` は、現在のホップバイホップヘッダーがリクエストに存在するかどうかを確認します。
* `if !copiedHeaders` ブロックは、まだヘッダーマップがコピーされていない場合にのみ実行されます。
* `outreq.Header = make(http.Header)`: 新しい空の `http.Header` マップを作成します。
* `copyHeader(outreq.Header, req.Header)`: 元の `req.Header` の内容を新しく作成した `outreq.Header` にコピーします。これにより、元のリクエストヘッダーが変更されることを防ぎ、`outreq.Header` が独立したマップになります。
* `copiedHeaders = true`: ヘッダーマップがコピーされたことを示します。
* `outreq.Header.Del(h)`: 存在する場合、現在のホップバイホップヘッダーを `outreq.Header` から削除します。
このロジックにより、`ReverseProxy` はクライアントから受け取ったリクエストから、バックエンドに転送すべきではないすべてのホップバイホップヘッダーを確実に削除します。
### `reverseproxy_test.go` の変更点
テストケース `TestReverseProxy` に、`Upgrade` ヘッダーのテストが追加されました。
1. **バックエンドハンドラーでの `Upgrade` ヘッダーのチェック**:
```go
if c := r.Header.Get("Upgrade"); c != "" {
t.Errorf("handler got Keep-Alive header value %q", c)
}
```
これは、リバースプロキシを介してバックエンドに転送されたリクエストに `Upgrade` ヘッダーが含まれていないことを検証します。もし含まれていればテストは失敗します。
2. **クライアントリクエストへの `Upgrade` ヘッダーの追加**:
```go
getReq.Header.Set("Upgrade", "foo")
```
テスト用のクライアントリクエストに `Upgrade: foo` ヘッダーを追加し、このヘッダーがリバースプロキシによって正しく削除されることを確認するための準備をします。
これらのテストの追加により、`ReverseProxy` が `Upgrade` ヘッダーを含むホップバイホップヘッダーを正しく削除するようになったことが保証されます。
## 関連リンク
* Go Issue 2735: [https://github.com/golang/go/issues/2735](https://github.com/golang/go/issues/2735)
* Go CL 7470048: [https://golang.org/cl/7470048](https://golang.org/cl/7470048) (Gerrit Code Review)
* Go `net/http/httputil` パッケージドキュメント: [https://pkg.go.dev/net/http/httputil](https://pkg.go.dev/net/http/httputil)
## 参考にした情報源リンク
* RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (特にセクション 13.5.1 "Connection" Header Field): [https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1](https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1)
* MDN Web Docs - HTTP headers: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
* Wikipedia - リバースプロキシ: [https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B9%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7](https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B9%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7)
* Go言語の `http.Header` の正規化について (Stack Overflowなど): [https://stackoverflow.com/questions/30000000/go-http-header-canonicalization](https://stackoverflow.com/questions/30000000/go-http-header-canonicalization) (一般的な情報源として)