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

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

このコミットは、Go言語の標準ライブラリ net/http/httputil パッケージ内の DumpResponse 関数が、HTTPレスポンスのダンプ時に Content-Length ヘッダーを適切に含めるように修正するものです。これにより、ダンプされたレスポンスがより正確な情報を持つようになります。

コミット

commit 8f7664123087d1800c214b7dc6fb8ea169208ad6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Apr 17 14:03:05 2014 -0700

    net/http/httputil: include Content-Length in DumpResponse output
    
    Fixes #5357
    
    LGTM=nigeltao
    R=nigeltao
    CC=golang-codereviews
    https://golang.org/cl/87910050

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

https://github.com/golang/go/commit/8f7664123087d1800c214b7dc6fb8ea169208ad6

元コミット内容

diff --git a/src/pkg/net/http/httputil/dump.go b/src/pkg/net/http/httputil/dump.go
index ab1eab21bc..acd5618454 100644
--- a/src/pkg/net/http/httputil/dump.go
+++ b/src/pkg/net/http/httputil/dump.go
@@ -7,6 +7,7 @@ package httputil
 import (
 	"bufio"
 	"bytes"
+"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -230,14 +231,31 @@ func DumpRequest(req *http.Request, body bool) (dump []byte, err error) {
 	return
 }
 
+// errNoBody is a sentinel error value used by failureToReadBody so we can detect
+// that the lack of body was intentional.
+var errNoBody = errors.New("sentinel error value")
+
+// failureToReadBody is a io.ReadCloser that just returns errNoBody on
+// Read.  It's swapped in when we don't actually want to consume the
+// body, but need a non-nil one, and want to distinguish the error
+// from reading the dummy body.
+type failureToReadBody struct{}
+
+func (failureToReadBody) Read([]byte) (int, error) { return 0, errNoBody }
+func (failureToReadBody) Close() error             { return nil }
+
+var emptyBody = ioutil.NopCloser(strings.NewReader(""))
+
 // DumpResponse is like DumpRequest but dumps a response.
 func DumpResponse(resp *http.Response, body bool) (dump []byte, err error) {
 	var b bytes.Buffer
 	save := resp.Body
 	savecl := resp.ContentLength
-\tif !body || resp.Body == nil {\n-\t\tresp.Body = nil\n-\t\tresp.ContentLength = 0\n+\n+\tif !body {\n+\t\tresp.Body = failureToReadBody{}\n+\t} else if resp.Body == nil {\n+\t\tresp.Body = emptyBody\n \t} else {\n \t\tsave, resp.Body, err = drainBody(resp.Body)\n \t\tif err != nil {\n@@ -245,11 +263,13 @@ func DumpResponse(resp *http.Response, body bool) (dump []byte, err error) {\n \t\t}\n \t}\n \terr = resp.Write(&b)\n+\tif err == errNoBody {\n+\t\terr = nil\n+\t}\n \tresp.Body = save
 	resp.ContentLength = savecl
 	if err != nil {
-\t\treturn\n+\t\treturn nil, err\n \t}\n-\tdump = b.Bytes()\n-\treturn\n+\treturn b.Bytes(), nil\n }\ndiff --git a/src/pkg/net/http/httputil/dump_test.go b/src/pkg/net/http/httputil/dump_test.go
index a1dbfc39d6..c2902c8ec5 100644
--- a/src/pkg/net/http/httputil/dump_test.go
+++ b/src/pkg/net/http/httputil/dump_test.go
@@ -11,6 +11,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/url"
+"strings"
 	"testing"
 )
 
@@ -176,3 +177,82 @@ func mustNewRequest(method, url string, body io.Reader) *http.Request {
 	}\n 	return req
 }\n+\n+var dumpResTests = []struct {\n+\tres  *http.Response\n+\tbody bool\n+\twant string\n+}{\n+\t{\n+\t\tres: &http.Response{\n+\t\t\tStatus:        "200 OK",\n+\t\t\tStatusCode:    200,\n+\t\t\tProto:         "HTTP/1.1",\n+\t\t\tProtoMajor:    1,\n+\t\t\tProtoMinor:    1,\n+\t\t\tContentLength: 50,\n+\t\t\tHeader: http.Header{\n+\t\t\t\t"Foo": []string{"Bar"},\n+\t\t\t},\n+\t\t\tBody: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used\n+\t\t},\n+\t\tbody: false, // to verify we see 50, not empty or 3.\n+\t\twant: `HTTP/1.1 200 OK\n+Content-Length: 50\n+Foo: Bar`,\n+\t},\n+\n+\t{\n+\t\tres: &http.Response{\n+\t\t\tStatus:        "200 OK",\n+\t\t\tStatusCode:    200,\n+\t\t\tProto:         "HTTP/1.1",\n+\t\t\tProtoMajor:    1,\n+\t\t\tProtoMinor:    1,\n+\t\t\tContentLength: 3,\n+\t\t\tBody:          ioutil.NopCloser(strings.NewReader("foo")),\n+\t\t},\n+\t\tbody: true,\n+\t\twant: `HTTP/1.1 200 OK\n+Content-Length: 3\n+\n+foo`,\n+\t},\n+\n+\t{\n+\t\tres: &http.Response{\n+\t\t\tStatus:           "200 OK",\n+\t\t\tStatusCode:       200,\n+\t\t\tProto:            "HTTP/1.1",\n+\t\t\tProtoMajor:       1,\n+\t\t\tProtoMinor:       1,\n+\t\t\tContentLength:    -1,\n+\t\t\tBody:             ioutil.NopCloser(strings.NewReader("foo")),\n+\t\t\tTransferEncoding: []string{"chunked"},\n+\t\t},\n+\t\tbody: true,\n+\t\twant: `HTTP/1.1 200 OK\n+Transfer-Encoding: chunked\n+\n+3\n+foo\n+0`,\n+\t},\n+}\n+\n+func TestDumpResponse(t *testing.T) {\n+\tfor i, tt := range dumpResTests {\n+\t\tgotb, err := DumpResponse(tt.res, tt.body)\n+\t\tif err != nil {\n+\t\t\tt.Errorf("%d. DumpResponse = %v", i, err)\n+\t\t\tcontinue\n+\t\t}\n+\t\tgot := string(gotb)\n+\t\tgot = strings.TrimSpace(got)\n+\t\tgot = strings.Replace(got, "\\r", "", -1)\n+\n+\t\tif got != tt.want {\n+\t\t\tt.Errorf("%d.\\nDumpResponse got:\\n%s\\n\\nWant:\\n%s\\n", i, got, tt.want)\n+\t\t}\n+\t}\n+}\n```

## 変更の背景

このコミットは、`net/http/httputil` パッケージの `DumpResponse` 関数が、HTTPレスポンスをダンプする際に `Content-Length` ヘッダーを正しく出力しないという問題(`Fixes #5357` で言及されている問題)を解決するために行われました。

以前の `DumpResponse` の実装では、`body` 引数が `false` の場合、つまりレスポンスボディをダンプしない設定の場合に、`resp.Body` を `nil` に設定し、`resp.ContentLength` を `0` に設定していました。これにより、`http.Response.Write` メソッドがレスポンスを書き出す際に、本来の `Content-Length` の値が失われ、ダンプ結果に反映されないという問題がありました。

特に、`Content-Length` ヘッダーはHTTPメッセージのボディの長さを正確に伝えるために非常に重要であり、デバッグやプロトコル解析の際に不可欠な情報です。この情報が欠落していると、ダンプされたレスポンスの解釈が不正確になる可能性がありました。

この修正は、`DumpResponse` が `Content-Length` を含む、より正確で完全なHTTPレスポンスのダンプを提供できるようにすることを目的としています。

## 前提知識の解説

### `net/http/httputil` パッケージ

`net/http/httputil` は、Go言語の標準ライブラリ `net/http` パッケージを補完するユーティリティ機能を提供するパッケージです。主にHTTPリクエストやレスポンスのダンプ(内容の文字列化)、リバースプロキシの実装、接続の再利用などの機能を提供します。デバッグやネットワークトラフィックの検査において非常に有用です。

### `http.Response` 構造体

`http.Response` は、HTTPレスポンスを表すGo言語の構造体です。この構造体には、ステータスコード、プロトコルバージョン、ヘッダー、そしてレスポンスボディなどの情報が含まれます。
- `Status`: レスポンスのステータス文字列(例: "200 OK")。
- `StatusCode`: レスポンスの数値ステータスコード(例: 200)。
- `Proto`: 使用されたHTTPプロトコル(例: "HTTP/1.1")。
- `Header`: HTTPヘッダーを表す `http.Header` マップ。
- `Body`: レスポンスボディを表す `io.ReadCloser` インターフェース。
- `ContentLength`: レスポンスボディの長さ(バイト単位)。不明な場合は `-1`。

### `DumpResponse` 関数

`httputil.DumpResponse` 関数は、与えられた `http.Response` オブジェクトの内容をバイトスライスとしてダンプ(文字列化)します。
- `resp *http.Response`: ダンプするHTTPレスポンスオブジェクト。
- `body bool`: レスポンスボディを含めるかどうかを示すフラグ。`true` の場合、ボディもダンプに含まれます。

この関数は、デバッグ目的でHTTPレスポンスの生データを検査する際によく使用されます。

### `Content-Length` ヘッダー

`Content-Length` はHTTPヘッダーの一つで、HTTPメッセージのエンティティボディ(ペイロード)のバイト単位のサイズを示します。これは、受信側がメッセージの終わりを判断するために使用される重要なヘッダーです。特に、持続的接続(Persistent Connection)において、次のリクエストを送信するタイミングを決定するために不可欠です。

### `io.ReadCloser` インターフェース

`io.ReadCloser` は、Go言語の `io` パッケージで定義されているインターフェースで、`io.Reader` と `io.Closer` の両方のインターフェースを組み合わせたものです。
- `Read(p []byte) (n int, err error)`: データを読み込みます。
- `Close() error`: リソースを閉じます。
`http.Response.Body` はこの `io.ReadCloser` 型であり、レスポンスボディのストリームを読み込み、使用後に閉じる責任があることを示します。

### `ioutil.NopCloser` と `strings.NewReader`

- `strings.NewReader(s string)`: 文字列 `s` からデータを読み込む `io.Reader` を返します。これは、メモリ上の文字列をファイルのように扱う場合に便利です。
- `ioutil.NopCloser(r io.Reader)`: 与えられた `io.Reader` を `io.ReadCloser` にラップします。この `io.ReadCloser` の `Close` メソッドは何も行いません。これは、`io.ReadCloser` が期待されるが、基になるリーダーが閉じる必要がない場合(例: メモリ上のデータ)に便利です。

## 技術的詳細

このコミットの主要な変更点は、`DumpResponse` 関数が `body` 引数の値に応じて `resp.Body` の扱いを改善し、`Content-Length` ヘッダーが常に正しくダンプされるようにしたことです。

### 変更前(問題点)

変更前は、`DumpResponse` 関数内で `body` 引数が `false` の場合、または `resp.Body` が `nil` の場合、以下のように処理されていました。

```go
if !body || resp.Body == nil {
    resp.Body = nil
    resp.ContentLength = 0
}

このコードは、ボディをダンプしない場合に resp.Bodynil に、resp.ContentLength0 に強制的に設定していました。しかし、http.Response.Write メソッドは、Content-Length ヘッダーを生成する際に resp.ContentLength フィールドの値を参照します。したがって、この強制的な 0 設定により、本来の Content-Length の値が失われ、ダンプ結果に反映されませんでした。

変更後(修正内容)

修正後のコードは、この問題を解決するために以下の変更を導入しました。

  1. errNoBodyfailureToReadBody の導入:

    • errNoBody は、ボディを読み込むべきではない場合に Read メソッドが返す特別なエラー値として定義されました。
    • failureToReadBodyio.ReadCloser インターフェースを実装する新しい型です。この型の Read メソッドは常に 0, errNoBody を返し、Close メソッドは何も行いません。
  2. DumpResponse 内の resp.Body の扱い:

    • if !body の場合(ボディをダンプしない場合): resp.BodyfailureToReadBody{} に設定されます。これにより、http.Response.Write がボディを読み込もうとした際に errNoBody が返され、ボディの読み込みが意図的にスキップされたことを示します。しかし、resp.ContentLength は元の値が保持されるため、Write メソッドは正しい Content-Length ヘッダーを生成できます。
    • else if resp.Body == nil の場合(元のボディが nil の場合): resp.BodyemptyBody に設定されます。emptyBodyioutil.NopCloser(strings.NewReader("")) で作成された空のボディであり、http.Response.Write がボディを処理する際にエラーを発生させずに空のボディとして扱われます。
    • それ以外の場合(ボディをダンプする場合): 既存の drainBody 関数を使用してボディを読み込み、元のボディを保存します。これは変更前と同じ動作です。
  3. エラーハンドリングの改善: resp.Write(&b) の呼び出し後、返されたエラーが errNoBody であった場合、そのエラーは nil に上書きされます。これは、errNoBody が意図的なボディのスキップを示すための内部的なシグナルであり、外部にエラーとして伝播すべきではないためです。これにより、DumpResponse はボディをダンプしない場合でもエラーを返さずに正常に完了します。

これらの変更により、DumpResponsebody 引数の値に関わらず、http.Response.WriteContent-Length ヘッダーを正しく生成するために必要な resp.ContentLength の値を保持し、かつボディの読み込みを適切に制御できるようになりました。

テストの追加

dump_test.goTestDumpResponse という新しいテスト関数と dumpResTests というテストケースのスライスが追加されました。これらのテストケースは、Content-Length が正しくダンプされること、ボディがダンプされる場合とされない場合の両方で期待される出力が得られることを検証します。特に、Content-Length: 50 のレスポンスが body: false の場合でも正しく出力されるテストケースが含まれており、このコミットの目的を直接検証しています。

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

src/pkg/net/http/httputil/dump.go

--- a/src/pkg/net/http/httputil/dump.go
+++ b/src/pkg/net/http/httputil/dump.go
@@ -7,6 +7,7 @@ package httputil
 import (
 	"bufio"
 	"bytes"
+"errors" // 追加
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -230,14 +231,31 @@ func DumpRequest(req *http.Request, body bool) (dump []byte, err error) {
 	return
 }
 
+// errNoBody は、failureToReadBody が使用するセンチネルエラー値で、
+// ボディがないことが意図的であることを検出するために使用されます。
+var errNoBody = errors.New("sentinel error value")
+
+// failureToReadBody は io.ReadCloser で、Read 時に errNoBody を返します。
+// ボディを実際に消費したくないが、nil ではないボディが必要な場合、
+// およびダミーボディの読み込みからのエラーを区別したい場合に置き換えられます。
+type failureToReadBody struct{}
+
+func (failureToReadBody) Read([]byte) (int, error) { return 0, errNoBody }
+func (failureToReadBody) Close() error             { return nil }
+
+var emptyBody = ioutil.NopCloser(strings.NewReader("")) // 追加
+
 // DumpResponse is like DumpRequest but dumps a response.
 func DumpResponse(resp *http.Response, body bool) (dump []byte, err error) {
 	var b bytes.Buffer
 	save := resp.Body
 	savecl := resp.ContentLength
-\tif !body || resp.Body == nil {\n-\t\tresp.Body = nil\n-\t\tresp.ContentLength = 0\n+\n+\tif !body { // 変更
+\t\tresp.Body = failureToReadBody{} // 変更
+\t} else if resp.Body == nil { // 変更
+\t\tresp.Body = emptyBody // 変更
 \t} else {
  	\tsave, resp.Body, err = drainBody(resp.Body)
  	\tif err != nil {
@@ -245,11 +263,13 @@ func DumpResponse(resp *http.Response, body bool) (dump []byte, err error) {
  	\t}\n \t}\n \terr = resp.Write(&b)\n+\tif err == errNoBody { // 追加
+\t\terr = nil // 追加
+\t}\n \tresp.Body = save
 	resp.ContentLength = savecl
 	if err != nil {
-\t\treturn\n+\t\treturn nil, err // 変更
 \t}\n-\tdump = b.Bytes()\n-\treturn\n+\treturn b.Bytes(), nil // 変更
 }\n```

### `src/pkg/net/http/httputil/dump_test.go`

```diff
--- a/src/pkg/net/http/httputil/dump_test.go
+++ b/src/pkg/net/http/httputil/dump_test.go
@@ -11,6 +11,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/url"
+"strings" // 追加
 	"testing"
 )
 
@@ -176,3 +177,82 @@ func mustNewRequest(method, url string, body io.Reader) *http.Request {
 	}\n 	return req
 }\n+\n+// dumpResTests は DumpResponse のテストケースを定義します。
+var dumpResTests = []struct {
+	res  *http.Response
+	body bool
+	want string
+}{
+	{
+		res: &http.Response{
+			Status:        "200 OK",
+			StatusCode:    200,
+			Proto:         "HTTP/1.1",
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+			ContentLength: 50, // Content-Length が設定されているケース
+			Header: http.Header{
+				"Foo": []string{"Bar"},
+			},
+			Body: ioutil.NopCloser(strings.NewReader("foo")), // ボディは使用されない
+		},
+		body: false, // ボディをダンプしない設定
+		want: `HTTP/1.1 200 OK
+Content-Length: 50
+Foo: Bar`, // Content-Length が正しく出力されることを期待
+	},
+
+	{
+		res: &http.Response{
+			Status:        "200 OK",
+			StatusCode:    200,
+			Proto:         "HTTP/1.1",
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+			ContentLength: 3, // Content-Length が設定されているケース
+			Body:          ioutil.NopCloser(strings.NewReader("foo")),
+		},
+		body: true, // ボディをダンプする設定
+		want: `HTTP/1.1 200 OK
+Content-Length: 3
+
+foo`, // Content-Length とボディが正しく出力されることを期待
+	},
+
+	{
+		res: &http.Response{
+			Status:           "200 OK",
+			StatusCode:       200,
+			Proto:            "HTTP/1.1",
+			ProtoMajor:       1,
+			ProtoMinor:       1,
+			ContentLength:    -1, // Content-Length が不明なケース(Transfer-Encoding: chunked)
+			Body:             ioutil.NopCloser(strings.NewReader("foo")),
+			TransferEncoding: []string{"chunked"},
+		},
+		body: true, // ボディをダンプする設定
+		want: `HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+3
+foo
+0`, // Transfer-Encoding が正しく出力されることを期待
+	},
+}
+
+// TestDumpResponse は DumpResponse 関数をテストします。
+func TestDumpResponse(t *testing.T) {
+	for i, tt := range dumpResTests {
+		gotb, err := DumpResponse(tt.res, tt.body)
+		if err != nil {
+			t.Errorf("%d. DumpResponse = %v", i, err)
+			continue
+		}
+		got := string(gotb)
+		got = strings.TrimSpace(got)
+		got = strings.Replace(got, "\r", "", -1)
+
+		if got != tt.want {
+			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
+		}
+	}
+}

コアとなるコードの解説

src/pkg/net/http/httputil/dump.go の変更点

  1. import "errors" の追加: errors パッケージがインポートされ、カスタムエラー errNoBody を定義するために使用されます。

  2. errNoBody 変数の定義: var errNoBody = errors.New("sentinel error value") これは、DumpResponse がボディを読み込むべきではない場合に、failureToReadBody 型の Read メソッドが返す特別なエラー値です。このエラーは、ボディの読み込みが意図的にスキップされたことを示す「センチネルエラー」として機能します。

  3. failureToReadBody 型の定義:

    type failureToReadBody struct{}
    func (failureToReadBody) Read([]byte) (int, error) { return 0, errNoBody }
    func (failureToReadBody) Close() error             { return nil }
    

    この新しい型は io.ReadCloser インターフェースを実装します。

    • Read メソッドは常に 0 バイトを読み込み、errNoBody を返します。これにより、http.Response.Write がボディを読み込もうとした際に、ボディがないことを安全に伝えることができます。
    • Close メソッドは何も行いません。これは、このダミーボディが閉じるべきリソースを持たないためです。
  4. emptyBody 変数の定義: var emptyBody = ioutil.NopCloser(strings.NewReader("")) これは、元の resp.Bodynil であった場合に resp.Body に設定される空の io.ReadCloser です。strings.NewReader("") で空の文字列から読み込むリーダーを作成し、それを ioutil.NopCloserio.ReadCloser にラップしています。これにより、http.Response.Write がボディを処理する際に、エラーを発生させずに空のボディとして扱われます。

  5. DumpResponse 関数内の条件分岐の変更:

    -	if !body || resp.Body == nil {
    -		resp.Body = nil
    -		resp.ContentLength = 0
    +	if !body {
    +		resp.Body = failureToReadBody{}
    +	} else if resp.Body == nil {
    +		resp.Body = emptyBody
    	} else {
    		// ... 既存の drainBody 呼び出し ...
    	}
    
    • if !body ブロック: body 引数が false の場合(ボディをダンプしない場合)、resp.BodyfailureToReadBody{} に設定されます。これにより、http.Response.Write はボディを読み込もうとしますが、errNoBody を受け取ることで、ボディの読み込みを安全にスキップします。重要なのは、このとき resp.ContentLength は変更されないため、元の Content-Length の値が保持され、ダンプ結果に反映される点です。
    • else if resp.Body == nil ブロック: body 引数が true で、かつ元の resp.Bodynil であった場合、resp.BodyemptyBody に設定されます。これにより、http.Response.Write は空のボディとして処理し、エラーを発生させません。
    • else ブロック: body 引数が true で、かつ元の resp.Bodynil でない場合、既存の drainBody 関数が呼び出され、ボディが読み込まれます。
  6. エラーハンドリングの追加:

    	err = resp.Write(&b)
    +	if err == errNoBody {
    +		err = nil
    +	}
    

    resp.Write(&b) の呼び出し後、返されたエラーが errNoBody であった場合、そのエラーは nil に上書きされます。これは、errNoBodyDumpResponse の内部的なロジックでボディのスキップを示すために使用されるものであり、外部にエラーとして報告すべきではないためです。これにより、ボディをダンプしない場合でも、関数は正常に完了します。

  7. 戻り値の変更:

    -	return
    +	return nil, err // 変更
    	// ...
    -	dump = b.Bytes()
    -	return
    +	return b.Bytes(), nil // 変更
    

    エラーが発生した場合の戻り値の形式が return から return nil, err に、成功した場合の戻り値が dump = b.Bytes(); return から return b.Bytes(), nil に変更されました。これは、Goのエラーハンドリングの慣習に沿った明示的なエラーと戻り値の指定です。

src/pkg/net/http/httputil/dump_test.go の変更点

  1. import "strings" の追加: strings パッケージがインポートされ、strings.NewReader を使用してテスト用のボディを作成するために使用されます。

  2. dumpResTests 変数の追加: DumpResponse 関数のテストケースを構造体のスライスとして定義しています。各テストケースは、入力となる http.Responsebody フラグ、そして期待されるダンプ結果 (want 文字列) を含みます。

    • 最初のテストケースは、Content-Length: 50 が設定されたレスポンスで body: false の場合でも、Content-Length ヘッダーが正しくダンプされることを検証します。これがこのコミットの主要な修正点を直接テストするものです。
    • 2番目のテストケースは、Content-Length: 3 が設定されたレスポンスで body: true の場合、Content-Length とボディの両方が正しくダンプされることを検証します。
    • 3番目のテストケースは、Content-Length: -1(つまり Transfer-Encoding: chunked)のレスポンスで body: true の場合、Transfer-Encoding ヘッダーとチャンク化されたボディが正しくダンプされることを検証します。
  3. TestDumpResponse 関数の追加: dumpResTests スライスをイテレートし、各テストケースに対して DumpResponse を呼び出し、結果が期待値と一致するかを検証します。

    • strings.TrimSpace(got)strings.Replace(got, "\r", "", -1) を使用して、ダンプ結果から余分な空白やCR文字を削除し、比較を安定させています。

これらのテストの追加により、DumpResponse の修正が意図通りに機能し、既存の動作を壊していないことが保証されます。

関連リンク

参考にした情報源リンク