[インデックス 17686] ファイルの概要
コミット
commit d9f034dc9eeea52e42a3138dcc8677db3554c879
Author: Russ Cox <rsc@golang.org>
Date: Mon Sep 23 17:16:59 2013 -0400
net/http: accept Content-Range for entire file
Fixes a bug reported privately.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13846043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d9f034dc9eeea52e42a3138dcc8677db3554c879
元コミット内容
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおいて、HTTPの Content-Range
ヘッダの処理に関するバグを修正するものです。具体的には、ファイル全体を要求する Range
ヘッダが、誤って不正なリクエスト(攻撃の可能性)として扱われ、416 Requested Range Not Satisfiable
エラーが返される問題を解決します。
変更の背景
HTTP/1.1では、クライアントは Range
ヘッダを使用して、リソース(ファイルなど)の特定の部分のみを要求することができます。例えば、Range: bytes=0-999
は、ファイルが1000バイトの場合、最初の1000バイト(つまりファイル全体)を要求します。サーバーはこれに対し、206 Partial Content
ステータスコードと Content-Range
ヘッダを返して、要求された範囲のデータを提供します。
このコミット以前の net/http
パッケージの実装では、serveContent
関数内で、要求されたレンジの合計サイズがファイルの実際のサイズと「等しいかそれ以上」の場合に、そのリクエストを不正なもの(潜在的な攻撃)と判断し、416 Requested Range Not Satisfiable
エラーを返していました。
このロジックの欠陥は、クライアントがファイル全体を要求する有効な Range
ヘッダ(例: Range: bytes=0-(fileSize-1)
)を送信した場合に顕在化しました。この場合、要求されたレンジの合計サイズはファイルの実際のサイズと完全に一致します。しかし、既存の >=
(以上) の比較によって、この有効なリクエストが誤って不正なものと判断され、サーバーが正しく応答できないというバグが発生していました。この問題はプライベートに報告されたとのことです。
前提知識の解説
HTTP Range Requests (範囲リクエスト)
HTTPの範囲リクエストは、クライアントがリソース全体ではなく、その一部のみを要求するためのメカニズムです。これは、大きなファイルのダウンロードを中断・再開したり、メディアストリーミングで特定のセクションにシークしたりする際に非常に有用です。
-
Range
ヘッダ: クライアントがサーバーに送信するリクエストヘッダです。- 例:
Range: bytes=0-499
(最初の500バイトを要求) - 例:
Range: bytes=500-999
(501バイト目から1000バイト目までを要求) - 例:
Range: bytes=-500
(最後の500バイトを要求) - 例:
Range: bytes=500-
(501バイト目からファイルの最後までを要求) - 複数の範囲を要求することも可能です:
Range: bytes=0-499, 900-999
- 例:
-
Content-Range
ヘッダ: サーバーがクライアントに送信するレスポンスヘッダです。206 Partial Content
ステータスコードと共に使用され、返されるデータの範囲とリソース全体のサイズを示します。- 例:
Content-Range: bytes 0-499/1000
(1000バイトのファイルのうち、0-499バイト目を返していることを示す)
- 例:
HTTP ステータスコード
200 OK
: リクエストが成功し、リソース全体が返されたことを示します。206 Partial Content
: クライアントのRange
ヘッダに応答して、リソースの一部のみが返されたことを示します。416 Requested Range Not Satisfiable
: クライアントが要求した範囲が、リソースの現在のサイズに対して無効であることを示します。例えば、存在しない範囲を要求した場合や、範囲の合計がリソースサイズを超える場合などです。
Go言語 net/http
パッケージ
net/http
はGo言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。このコミットで変更された serveContent
関数は、ファイルやその他のコンテンツをHTTPレスポンスとして提供する際に、特に範囲リクエストの処理を担当します。
技術的詳細
このコミットの核心的な変更は、src/pkg/net/http/fs.go
ファイル内の serveContent
関数における条件式の修正です。
変更前:
if sumRangesSize(ranges) >= size {
// The total number of bytes in all the ranges
// is larger than the size of the file by
// itself, so this is probably an attack, or a
変更後:
if sumRangesSize(ranges) > size {
// The total number of bytes in all the ranges
// is larger than the size of the file by
// itself, so this is probably an attack, or a
ここで、sumRangesSize(ranges)
はクライアントが要求したすべての範囲の合計バイト数を計算します。size
は提供しようとしているファイルの実際の合計バイト数です。
元のコードでは、sumRangesSize(ranges)
が size
と「等しいかそれ以上」の場合に、そのリクエストを不正なものと判断していました。これは、ファイル全体を要求する有効な Range
ヘッダ(例: Range: bytes=0-999
for a 1000-byte file)が、sumRangesSize(ranges)
が size
と等しくなるため、誤って不正なリクエストとして扱われる原因となっていました。
修正後のコードでは、比較演算子が >=
から >
に変更されています。これにより、sumRangesSize(ranges)
が size
と「厳密に大きい」場合にのみ、リクエストが不正であると判断されるようになります。つまり、要求された範囲の合計サイズがファイルの実際のサイズと完全に一致する場合(ファイル全体を要求する有効なケース)は、もはや不正なリクエストとは見なされず、正常に処理されるようになります。
この変更により、net/http
サーバーは、ファイル全体を要求する有効な Range
ヘッダに対して 206 Partial Content
を返すか、あるいは 200 OK
を返す(範囲リクエストが無視される場合など)ことができるようになり、HTTP仕様に準拠した振る舞いをします。
また、src/pkg/net/http/fs_test.go
には、この修正を検証するための新しいテストケースが追加されています。特に、testFileLen
を使用してファイル全体を要求する様々なシナリオがテストされており、修正が意図通りに機能することを確認しています。
コアとなるコードの変更箇所
src/pkg/net/http/fs.go
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -173,7 +173,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
return
}
- if sumRangesSize(ranges) >= size {
+ if sumRangesSize(ranges) > size {
// The total number of bytes in all the ranges
// is larger than the size of the file by
// itself, so this is probably an attack, or a
src/pkg/net/http/fs_test.go
--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -22,6 +22,7 @@ import (
"path/filepath"
"regexp"
"runtime"
+ "strconv"
"strings"
"testing"
"time"
@@ -36,6 +37,8 @@ type wantRange struct {
start, end int64 // range [start,end)
}
+var itoa = strconv.Itoa
+
var ServeFileRangeTests = []struct {
r string
code int
@@ -50,7 +53,11 @@ var ServeFileRangeTests = []struct {
{r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
{r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
{r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
+ {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
{r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
+ {r: "bytes=0-" + itoa(testFileLen-2), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
+ {r: "bytes=0-" + itoa(testFileLen-1), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
+ {r: "bytes=0-" + itoa(testFileLen), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
}
func TestServeFile(t *testing.T) {
コアとなるコードの解説
src/pkg/net/http/fs.go
の変更
serveContent
関数は、HTTPリクエストに対するコンテンツの提供を担当します。この関数内で、クライアントから送られてきた Range
ヘッダを解析し、要求された範囲がファイルのサイズに対して有効かどうかを検証するロジックが存在します。
変更された行 if sumRangesSize(ranges) > size {
は、この検証ロジックの核心です。
ranges
: クライアントが要求したバイト範囲のリスト。sumRangesSize(ranges)
:ranges
リスト内のすべてのバイト範囲の合計サイズを計算するヘルパー関数。size
: 提供されるファイルの実際の合計バイト数。
元のコードでは、sumRangesSize(ranges)
が size
と「等しいかそれ以上」の場合に、416 Requested Range Not Satisfiable
エラーを返していました。これは、例えば1000バイトのファイルに対して Range: bytes=0-999
というリクエストがあった場合、sumRangesSize
が1000を返し、size
も1000であるため、1000 >= 1000
が真となり、エラーが返されてしまうという問題がありました。
修正後は、比較が >
(より大きい) に変更されたため、sumRangesSize(ranges)
が size
と「厳密に大きい」場合にのみエラーが返されます。これにより、sumRangesSize(ranges)
が size
と等しい場合は、有効なリクエストとして処理が続行されるようになります。これは、ファイル全体を要求する有効な範囲リクエストを正しく処理するために不可欠な変更です。
src/pkg/net/http/fs_test.go
の変更
テストファイル fs_test.go
には、ServeFileRangeTests
というテストケースのスライスがあり、様々な Range
ヘッダのリクエストに対する serveContent
の期待される振る舞いを定義しています。
このコミットでは、strconv.Itoa
をインポートし、itoa
というエイリアスを定義しています。これは整数を文字列に変換するために使用されます。
追加されたテストケースは、特にファイル全体を要求するシナリオに焦点を当てています。
{r: "bytes=0-" + itoa(testFileLen-2), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
- ファイルサイズより2バイト少ない範囲を要求するケース。
{r: "bytes=0-" + itoa(testFileLen-1), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
- ファイル全体を正確にカバーする範囲(0から最後のバイトまで)を要求するケース。このケースが以前は誤ってエラーになっていました。
{r: "bytes=0-" + itoa(testFileLen), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
Range: bytes=0-N
の形式で、N
がファイルサイズと等しい場合。これは一部のHTTP実装でファイル全体を意味する慣習的なリクエストであり、このテストケースはnet/http
がこれを正しく処理することを確認します。wantRange{{0, testFileLen}}
は、このリクエストがファイル全体(0からtestFileLen
バイト目まで、つまりtestFileLen
バイト)として解釈されることを示しています。
これらのテストケースの追加により、serveContent
関数が Range
ヘッダを正しく解釈し、特にファイル全体を要求する有効なリクエストに対して 416
エラーを返さずに 206 Partial Content
を返すことを保証しています。
関連リンク
- Go Change-Id:
I2222222222222222222222222222222222222222
(コミットメッセージのhttps://golang.org/cl/13846043
に対応するGoのコードレビューシステムへのリンク)
参考にした情報源リンク
- RFC 7233: Hypertext Transfer Protocol (HTTP/1.1): Range Requests - https://tools.ietf.org/html/rfc7233
- Go
net/http
パッケージ ドキュメント - https://pkg.go.dev/net/http