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

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

このコミットは、Go言語のnet/http/cgiパッケージにおいて、子プロセスからの不正なCGIレスポンスに対するハンドリングを改善するものです。具体的には、CGIスクリプトがRFC 3875の規定に違反するレスポンスを返した場合に、HTTP 200 OKではなくHTTP 500 Internal Server Errorを返すように修正されています。これにより、サーバーが不正なCGIレスポンスを適切に処理し、クライアントに正確なエラー情報を提供できるようになります。

コミット

net/http/cgi: serve 500, not 200, on invalid responses from child processes

Per RFC 3875 section 6 rules.

Fixes #7198

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049

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

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

元コミット内容

commit d53251d4aba2820eb8f788be75a1832c6f14213b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Mar 11 22:55:15 2014 -0700

    net/http/cgi: serve 500, not 200, on invalid responses from child processes
    
    Per RFC 3875 section 6 rules.
    
    Fixes #7198
    
    LGTM=adg
    R=adg
    CC=golang-codereviews
    https://golang.org/cl/68960049

変更の背景

この変更の背景には、CGI (Common Gateway Interface) の仕様であるRFC 3875のセクション6に準拠する必要性がありました。以前の実装では、CGI子プロセスがHTTPヘッダーを適切に送信しなかったり、Content-Typeヘッダーが欠落していたりするような不正なレスポンスを返した場合でも、net/http/cgiハンドラがHTTP 200 OKステータスコードを返してしまう可能性がありました。これは、クライアント側から見ると、リクエストが成功したかのように見えてしまうため、問題の特定やデバッグを困難にしていました。

RFC 3875のセクション6では、CGIスクリプトからのレスポンスの形式が厳密に定義されており、特にヘッダーブロックの構造や必須ヘッダーの存在が規定されています。このコミットは、これらの規定に違反するレスポンスを「不正」とみなし、HTTP 500 Internal Server Errorを返すことで、サーバーがCGIスクリプトの出力の不備をクライアントに明確に伝えるように修正することを目的としています。これにより、CGIアプリケーションの堅牢性が向上し、エラーハンドリングがより正確になります。

具体的には、GoのIssue #7198で報告された問題に対応しています。この問題は、CGIスクリプトがヘッダーを全く返さなかったり、Content-Typeヘッダーを返さなかったりした場合に、GoのCGIハンドラが誤って200 OKを返してしまうというものでした。

前提知識の解説

CGI (Common Gateway Interface)

CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携するための標準的なインターフェースです。Webサーバーはクライアントからのリクエストを受け取ると、CGIスクリプトを起動し、リクエスト情報を環境変数や標準入力としてスクリプトに渡します。CGIスクリプトは処理結果を標準出力に書き出し、Webサーバーはその出力を受け取ってクライアントにHTTPレスポンスとして返します。

CGIスクリプトの出力は、まずHTTPヘッダーブロック(例: Content-Type: text/html)が続き、その後に空行を挟んでメッセージボディが続くという形式が一般的です。

HTTPステータスコード

HTTPステータスコードは、Webサーバーがクライアントのリクエストに対してどのような結果を返したかを示す3桁の数字です。

  • 200 OK: リクエストが成功し、要求された情報がレスポンスボディに含まれていることを示します。
  • 500 Internal Server Error: サーバーがリクエストを処理する際に予期せぬエラーに遭遇したことを示します。これは、サーバー側の問題、アプリケーションのバグ、または今回のようにCGIスクリプトからの不正な出力など、様々な原因で発生する可能性があります。

RFC 3875 (The Common Gateway Interface (CGI) Version 1.1)

RFC 3875は、CGIのバージョン1.1を定義する標準ドキュメントです。このドキュメントは、WebサーバーとCGIスクリプト間のインターフェース、データ交換の形式、およびエラー処理の規則を詳細に記述しています。

特に、**セクション6「Script Response」**は、CGIスクリプトがWebサーバーに返す出力の形式について規定しています。このセクションでは、CGIスクリプトの出力は、まずHTTPヘッダーフィールドのブロックで始まり、その後に空行が続き、最後にメッセージボディが続くことを明確にしています。また、Content-Typeヘッダーは、スクリプトがメッセージボディを返す場合に必須であるとされています。

RFC 3875のセクション6における「不正なレスポンス」とは、以下のような状況を指します。

  • 不正なヘッダーブロック: CGIレスポンスは、1つ以上のヘッダーフィールドと、それに続く空行で構成されるヘッダーブロックで始まる必要があります。ヘッダーフィールドの形式が不正(例: フィールド名: 値の構造が守られていない)であったり、ヘッダーとメッセージボディを区切る空行が欠落していたりする場合、レスポンスは不正とみなされます。
  • 不正なヘッダーフィールド値: StatusContent-TypeLocationなどの特定のヘッダーフィールドには、定義された構文と許容される値があります。必須ヘッダーに無効な値が提供されたり、サーバーが解析できない値であったりする場合も、レスポンスは不正になります。
  • 予期しない、または非準拠の出力: ヘッダーの前に任意のテキストが出力されたり、Content-Typeヘッダーがないのにバイナリデータが出力されたりするなど、スクリプトが予期されるHTTPレスポンス構造に従わない出力を生成した場合、サーバーはそれを不正と判断します。
  • 早期終了: スクリプトが完全で構文的に正しいレスポンスを生成する前に終了した場合も、不正なレスポンスとなります。

RFCでは、このような不正なCGIスクリプトからのレスポンスをサーバーが処理する責任があると規定しており、通常はクライアントに対して適切なHTTPエラーレスポンス(例: 500 Internal Server Error)を生成します。

技術的詳細

このコミットは、src/pkg/net/http/cgi/host.gosrc/pkg/net/http/cgi/matryoshka_test.goの2つのファイルに変更を加えています。

src/pkg/net/http/cgi/host.goの変更

host.goは、CGIハンドラの主要なロジックを実装しているファイルです。変更の核心は、CGI子プロセスからの出力ストリームを読み込み、HTTPヘッダーを解析する部分にあります。

  1. ヘッダー行のカウントと空行の検出: headerLinesという新しい変数が追加され、読み込んだヘッダー行の数をカウントします。また、sawBlankLineというブール変数が追加され、ヘッダーブロックの終わりを示す空行が検出されたかどうかを追跡します。

    	statusCode := 0
    	headerLines := 0 // 追加
    	sawBlankLine := false // 追加
    	for {
    		line, isPrefix, err := linebody.ReadLine()
    		// ...
    		if len(line) == 0 {
    			sawBlankLine = true // 空行を検出
    			break
    		}
    		headerLines++ // ヘッダー行をカウント
    		// ...
    	}
    
  2. 不正なヘッダーブロックのチェック: ヘッダーの読み込みループが終了した後、以下の条件で不正なヘッダーブロックをチェックします。

    • headerLines == 0: CGIスクリプトがヘッダーを全く返さなかった場合。
    • !sawBlankLine: ヘッダーブロックの終端を示す空行がなかった場合。

    これらの条件のいずれかが真であれば、rw.WriteHeader(http.StatusInternalServerError)を呼び出してHTTP 500ステータスコードを返し、エラーメッセージをログに出力して処理を終了します。

    	if headerLines == 0 || !sawBlankLine {
    		rw.WriteHeader(http.StatusInternalServerError)
    		h.printf("cgi: no headers")
    		return
    	}
    
  3. Content-Typeヘッダーのチェック: CGIスクリプトがメッセージボディを返す場合、Content-Typeヘッダーは必須です。この変更では、statusCodeがまだ設定されておらず(つまり、CGIスクリプトがStatusヘッダーを返さなかった場合)、かつContent-Typeヘッダーが空の場合に、HTTP 500を返すように修正されています。

    	if statusCode == 0 && headers.Get("Content-Type") == "" {
    		rw.WriteHeader(http.StatusInternalServerError)
    		h.printf("cgi: missing required Content-Type in headers")
    		return
    	}
    

src/pkg/net/http/cgi/matryoshka_test.goの変更

matryoshka_test.goは、CGIハンドラのテストケースを含むファイルです。このコミットでは、上記の変更を検証するための新しいテストケースが追加されています。

  1. 新しいテスト関数の追加:

    • Test500WithNoHeaders: CGIスクリプトが何もヘッダーを返さずに終了する場合をテストします。
    • Test500WithNoContentType: CGIスクリプトがContent-Lengthヘッダーのみを返し、Content-Typeヘッダーを返さない場合をテストします。
    • Test500WithEmptyHeaders: CGIスクリプトが空行のみを返し、ヘッダーを全く返さない場合をテストします。
  2. want500Testヘルパー関数の追加: これらの新しいテストケースは、want500Testというヘルパー関数を使用しています。この関数は、指定されたパスに対してCGIリクエストを実行し、レスポンスのHTTPステータスコードが500であることを検証します。

  3. TestBeChildCGIProcess内のCGIスクリプトの挙動定義: TestBeChildCGIProcessは、テスト中にCGI子プロセスとして実行される部分です。新しいテストケースに対応するため、REQUEST_URI環境変数の値に基づいて、CGIスクリプトが特定の不正なレスポンスを返すように分岐が追加されています。

    • /immediate-disconnect: os.Exit(0)を呼び出して即座に終了し、何も出力しない。
    • /no-content-type: Content-Lengthヘッダーのみを出力し、Content-Typeヘッダーを省略する。
    • /empty-headers: 空行のみを出力し、ヘッダーを全く出力しない。

これらのテストケースにより、CGIハンドラが不正なレスポンスに対して正しくHTTP 500を返すことが保証されます。

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

src/pkg/net/http/cgi/host.go

--- a/src/pkg/net/http/cgi/host.go
+++ b/src/pkg/net/http/cgi/host.go
@@ -223,6 +223,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	linebody := bufio.NewReaderSize(stdoutRead, 1024)
 	headers := make(http.Header)
 	statusCode := 0
+	headerLines := 0
+	sawBlankLine := false
 	for {
 		line, isPrefix, err := linebody.ReadLine()
 		if isPrefix {
@@ -239,8 +241,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 			return
 		}
 		if len(line) == 0 {
+			sawBlankLine = true
 			break
 		}
+		headerLines++
 		parts := strings.SplitN(string(line), ":", 2)
 		if len(parts) < 2 {
 			h.printf("cgi: bogus header line: %s", string(line))
@@ -266,6 +270,11 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 			headers.Add(header, val)
 		}
 	}
+	if headerLines == 0 || !sawBlankLine {
+		rw.WriteHeader(http.StatusInternalServerError)
+		h.printf("cgi: no headers")
+		return
+	}
 
 	if loc := headers.Get("Location"); loc != "" {
 		if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
@@ -277,6 +286,12 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		}
 	}
 
+	if statusCode == 0 && headers.Get("Content-Type") == "" {
+		rw.WriteHeader(http.StatusInternalServerError)
+		h.printf("cgi: missing required Content-Type in headers")
+		return
+	}
+
 	if statusCode == 0 {
 		statusCode = http.StatusOK
 	}

src/pkg/net/http/cgi/matryoshka_test.go

--- a/src/pkg/net/http/cgi/matryoshka_test.go
+++ b/src/pkg/net/http/cgi/matryoshka_test.go
@@ -128,6 +128,7 @@ func TestKillChildAfterCopyError(t *testing.T) {
 }
 
 // Test that a child handler writing only headers works.
+// golang.org/issue/7196
 func TestChildOnlyHeaders(t *testing.T) {
 	h := &Handler{
 		Path: os.Args[0],
@@ -143,6 +144,26 @@ func TestChildOnlyHeaders(t *testing.T) {
 	}
 }
 
+// golang.org/issue/7198
+func Test500WithNoHeaders(t *testing.T)     { want500Test(t, "/immediate-disconnect") }
+func Test500WithNoContentType(t *testing.T) { want500Test(t, "/no-content-type") }
+func Test500WithEmptyHeaders(t *testing.T)  { want500Test(t, "/empty-headers") }
+
+func want500Test(t *testing.T, path string) {
+	h := &Handler{
+		Path: os.Args[0],
+		Root: "/test.go",
+		Args: []string{"-test.run=TestBeChildCGIProcess"},
+	}
+	expectedMap := map[string]string{
+		"_body": "",
+	}
+	replay := runCgiTest(t, h, "GET "+path+" HTTP/1.0\\nHost: example.com\\n\\n", expectedMap)
+	if replay.Code != 500 {
+		t.Errorf("Got code %d; want 500", replay.Code)
+	}
+}
+
 type neverEnding byte
 
 func (b neverEnding) Read(p []byte) (n int, err error) {
@@ -158,6 +179,16 @@ func TestBeChildCGIProcess(t *testing.T) {
 		// Not in a CGI environment; skipping test.
 		return
 	}
+	switch os.Getenv("REQUEST_URI") {
+	case "/immediate-disconnect":
+		os.Exit(0)
+	case "/no-content-type":
+		fmt.Printf("Content-Length: 6\\n\\nHello\\n")
+		os.Exit(0)
+	case "/empty-headers":
+		fmt.Printf("\\nHello")
+		os.Exit(0)
+	}
 	Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		rw.Header().Set("X-Test-Header", "X-Test-Value")
 		req.ParseForm()

コアとなるコードの解説

src/pkg/net/http/cgi/host.go

  • headerLinessawBlankLineの導入: CGIスクリプトからの出力は、まずHTTPヘッダーが複数行続き、その後に空行が来て、最後にメッセージボディが続くという形式です。headerLinesは、このヘッダー部分で何行読み込んだかを追跡します。sawBlankLineは、ヘッダーとボディを区切る重要な空行が検出されたかどうかを記録します。これらの変数は、CGIレスポンスがRFC 3875の規定に準拠しているかを判断するための状態を保持します。

  • 不正なヘッダーブロックのチェック: if headerLines == 0 || !sawBlankLineの条件は、CGIスクリプトがヘッダーを全く返さなかった場合(headerLines == 0)や、ヘッダーの後に空行がなかった場合(!sawBlankLine)を検出します。RFC 3875では、ヘッダーブロックは少なくとも1つのヘッダー行とそれに続く空行で構成される必要があるため、これらのケースは不正なレスポンスとみなされます。この場合、http.StatusInternalServerError (500) をクライアントに返し、エラーメッセージをログに出力して処理を中断します。これにより、サーバーはCGIスクリプトの出力が不完全であることを明確に示します。

  • Content-Typeヘッダーのチェック: if statusCode == 0 && headers.Get("Content-Type") == ""の条件は、CGIスクリプトがHTTPステータスコードを明示的に設定せず(statusCode == 0はデフォルトの200 OKが適用される前)、かつContent-Typeヘッダーが欠落している場合に発動します。RFC 3875では、CGIスクリプトがメッセージボディを返す場合、Content-Typeヘッダーは必須とされています。この条件が真の場合も、http.StatusInternalServerError (500) を返し、エラーメッセージをログに出力します。これは、CGIスクリプトが必須ヘッダーを提供しなかったことを示し、クライアントがレスポンスボディを正しく解釈できない可能性を回避します。

src/pkg/net/http/cgi/matryoshka_test.go

  • Test500WithNoHeaders, Test500WithNoContentType, Test500WithEmptyHeaders: これらのテスト関数は、host.goで追加された不正なCGIレスポンスの検出ロジックを検証するために導入されました。それぞれ、CGIスクリプトが「ヘッダーなしで即座に終了する」「Content-Typeなしでレスポンスを返す」「空行のみを返す」という3つの異なる不正なシナリオをシミュレートします。

  • want500Testヘルパー関数: この関数は、上記3つのテスト関数で共通のテストロジックをカプセル化しています。CGIハンドラを設定し、特定のパスへのリクエストを実行し、その結果としてHTTP 500ステータスコードが返されることをアサートします。これにより、テストコードの重複が避けられ、可読性が向上します。

  • TestBeChildCGIProcess内のswitch os.Getenv("REQUEST_URI"): このswitch文は、テスト中にCGI子プロセスとして実行される際に、REQUEST_URI環境変数の値に基づいて異なる不正なCGIレスポンスを生成するためのものです。これにより、各テストケースが意図する不正なCGIスクリプトの挙動を正確にシミュレートできます。例えば、/immediate-disconnectの場合は何も出力せずに終了し、/no-content-typeの場合はContent-Typeヘッダーを省略したレスポンスを生成します。

これらの変更により、Goのnet/http/cgiパッケージは、RFC 3875の仕様に厳密に準拠し、CGI子プロセスからの不正なレスポンスに対してより堅牢なエラーハンドリングを提供するようになりました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語のnet/http/cgiパッケージにおいて、子プロセスからの不正なCGIレスポンスに対するハンドリングを改善するものです。具体的には、CGIスクリプトがRFC 3875の規定に違反するレスポンスを返した場合に、HTTP 200 OKではなくHTTP 500 Internal Server Errorを返すように修正されています。これにより、サーバーが不正なCGIレスポンスを適切に処理し、クライアントに正確なエラー情報を提供できるようになります。

コミット

net/http/cgi: serve 500, not 200, on invalid responses from child processes

Per RFC 3875 section 6 rules.

Fixes #7198

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049

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

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

元コミット内容

commit d53251d4aba2820eb8f788be75a1832c6f14213b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Mar 11 22:55:15 2014 -0700

    net/http/cgi: serve 500, not 200, on invalid responses from child processes
    
    Per RFC 3875 section 6 rules.
    
    Fixes #7198
    
    LGTM=adg
    R=adg
    CC=golang-codereviews
    https://golang.org/cl/68960049

変更の背景

この変更の背景には、CGI (Common Gateway Interface) の仕様であるRFC 3875のセクション6に準拠する必要性がありました。以前の実装では、CGI子プロセスがHTTPヘッダーを適切に送信しなかったり、Content-Typeヘッダーが欠落していたりするような不正なレスポンスを返した場合でも、net/http/cgiハンドラがHTTP 200 OKステータスコードを返してしまう可能性がありました。これは、クライアント側から見ると、リクエストが成功したかのように見えてしまうため、問題の特定やデバッグを困難にしていました。

RFC 3875のセクション6では、CGIスクリプトからのレスポンスの形式が厳密に定義されており、特にヘッダーブロックの構造や必須ヘッダーの存在が規定されています。このコミットは、これらの規定に違反するレスポンスを「不正」とみなし、HTTP 500 Internal Server Errorを返すことで、サーバーがCGIスクリプトの出力の不備をクライアントに明確に伝えるように修正することを目的としています。これにより、CGIアプリケーションの堅牢性が向上し、エラーハンドリングがより正確になります。

具体的には、GoのIssue #7198で報告された問題に対応しています。この問題は、CGIスクリプトがヘッダーを全く返さなかったり、Content-Typeヘッダーを返さなかったりした場合に、GoのCGIハンドラが誤って200 OKを返してしまうというものでした。

前提知識の解説

CGI (Common Gateway Interface)

CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携するための標準的なインターフェースです。Webサーバーはクライアントからのリクエストを受け取ると、CGIスクリプトを起動し、リクエスト情報を環境変数や標準入力としてスクリプトに渡します。CGIスクリプトは処理結果を標準出力に書き出し、Webサーバーはその出力を受け取ってクライアントにHTTPレスポンスとして返します。

CGIスクリプトの出力は、まずHTTPヘッダーブロック(例: Content-Type: text/html)が続き、その後に空行を挟んでメッセージボディが続くという形式が一般的です。

HTTPステータスコード

HTTPステータスコードは、Webサーバーがクライアントのリクエストに対してどのような結果を返したかを示す3桁の数字です。

  • 200 OK: リクエストが成功し、要求された情報がレスポンスボディに含まれていることを示します。
  • 500 Internal Server Error: サーバーがリクエストを処理する際に予期せぬエラーに遭遇したことを示します。これは、サーバー側の問題、アプリケーションのバグ、または今回のようにCGIスクリプトからの不正な出力など、様々な原因で発生する可能性があります。

RFC 3875 (The Common Gateway Interface (CGI) Version 1.1)

RFC 3875は、CGIのバージョン1.1を定義する標準ドキュメントです。このドキュメントは、WebサーバーとCGIスクリプト間のインターフェース、データ交換の形式、およびエラー処理の規則を詳細に記述しています。

特に、**セクション6「Script Response」**は、CGIスクリプトがWebサーバーに返す出力の形式について規定しています。このセクションでは、CGIスクリプトの出力は、まずHTTPヘッダーフィールドのブロックで始まり、その後に空行が続き、最後にメッセージボディが続くことを明確にしています。また、Content-Typeヘッダーは、スクリプトがメッセージボディを返す場合に必須であるとされています。

RFC 3875のセクション6における「不正なレスポンス」とは、以下のような状況を指します。

  • 不正なヘッダーブロック: CGIレスポンスは、1つ以上のヘッダーフィールドと、それに続く空行で構成されるヘッダーブロックで始まる必要があります。ヘッダーフィールドの形式が不正(例: フィールド名: 値の構造が守られていない)であったり、ヘッダーとメッセージボディを区切る空行が欠落していたりする場合、レスポンスは不正とみなされます。
  • 不正なヘッダーフィールド値: StatusContent-TypeLocationなどの特定のヘッダーフィールドには、定義された構文と許容される値があります。必須ヘッダーに無効な値が提供されたり、サーバーが解析できない値であったりする場合も、レスポンスは不正になります。
  • 予期しない、または非準拠の出力: ヘッダーの前に任意のテキストが出力されたり、Content-Typeヘッダーがないのにバイナリデータが出力されたりするなど、スクリプトが予期されるHTTPレスポンス構造に従わない出力を生成した場合、サーバーはそれを不正と判断します。
  • 早期終了: スクリプトが完全で構文的に正しいレスポンスを生成する前に終了した場合も、不正なレスポンスとなります。

RFCでは、このような不正なCGIスクリプトからのレスポンスをサーバーが処理する責任があると規定しており、通常はクライアントに対して適切なHTTPエラーレスポンス(例: 500 Internal Server Error)を生成します。

技術的詳細

このコミットは、src/pkg/net/http/cgi/host.gosrc/pkg/net/http/cgi/matryoshka_test.goの2つのファイルに変更を加えています。

src/pkg/net/http/cgi/host.goの変更

host.goは、CGIハンドラの主要なロジックを実装しているファイルです。変更の核心は、CGI子プロセスからの出力ストリームを読み込み、HTTPヘッダーを解析する部分にあります。

  1. ヘッダー行のカウントと空行の検出: headerLinesという新しい変数が追加され、読み込んだヘッダー行の数をカウントします。また、sawBlankLineというブール変数が追加され、ヘッダーブロックの終わりを示す空行が検出されたかどうかを追跡します。

    	statusCode := 0
    	headerLines := 0 // 追加
    	sawBlankLine := false // 追加
    	for {
    		line, isPrefix, err := linebody.ReadLine()
    		// ...
    		if len(line) == 0 {
    			sawBlankLine = true // 空行を検出
    			break
    		}
    		headerLines++ // ヘッダー行をカウント
    		// ...
    	}
    
  2. 不正なヘッダーブロックのチェック: ヘッダーの読み込みループが終了した後、以下の条件で不正なヘッダーブロックをチェックします。

    • headerLines == 0: CGIスクリプトがヘッダーを全く返さなかった場合。
    • !sawBlankLine: ヘッダーブロックの終端を示す空行がなかった場合。

    これらの条件のいずれかが真であれば、rw.WriteHeader(http.StatusInternalServerError)を呼び出してHTTP 500ステータスコードを返し、エラーメッセージをログに出力して処理を終了します。

    	if headerLines == 0 || !sawBlankLine {
    		rw.WriteHeader(http.StatusInternalServerError)
    		h.printf("cgi: no headers")
    		return
    	}
    
  3. Content-Typeヘッダーのチェック: CGIスクリプトがメッセージボディを返す場合、Content-Typeヘッダーは必須です。この変更では、statusCodeがまだ設定されておらず(つまり、CGIスクリプトがStatusヘッダーを返さなかった場合)、かつContent-Typeヘッダーが空の場合に、HTTP 500を返すように修正されています。

    	if statusCode == 0 && headers.Get("Content-Type") == "" {
    		rw.WriteHeader(http.StatusInternalServerError)
    		h.printf("cgi: missing required Content-Type in headers")
    		return
    	}
    

src/pkg/net/http/cgi/matryoshka_test.goの変更

matryoshka_test.goは、CGIハンドラのテストケースを含むファイルです。このコミットでは、上記の変更を検証するための新しいテストケースが追加されています。

  1. 新しいテスト関数の追加:

    • Test500WithNoHeaders: CGIスクリプトが何もヘッダーを返さずに終了する場合をテストします。
    • Test500WithNoContentType: CGIスクリプトがContent-Lengthヘッダーのみを返し、Content-Typeヘッダーを返さない場合をテストします。
    • Test500WithEmptyHeaders: CGIスクリプトが空行のみを返し、ヘッダーを全く返さない場合をテストします。
  2. want500Testヘルパー関数の追加: これらの新しいテストケースは、want500Testというヘルパー関数を使用しています。この関数は、指定されたパスに対してCGIリクエストを実行し、レスポンスのHTTPステータスコードが500であることを検証します。

  3. TestBeChildCGIProcess内のCGIスクリプトの挙動定義: TestBeChildCGIProcessは、テスト中にCGI子プロセスとして実行される部分です。新しいテストケースに対応するため、REQUEST_URI環境変数の値に基づいて、CGIスクリプトが特定の不正なレスポンスを返すように分岐が追加されています。

    • /immediate-disconnect: os.Exit(0)を呼び出して即座に終了し、何も出力しない。
    • /no-content-type: Content-Lengthヘッダーのみを出力し、Content-Typeヘッダーを省略する。
    • /empty-headers: 空行のみを出力し、ヘッダーを全く出力しない。

これらのテストケースにより、CGIハンドラが不正なレスポンスに対して正しくHTTP 500を返すことが保証されます。

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

src/pkg/net/http/cgi/host.go

--- a/src/pkg/net/http/cgi/host.go
+++ b/src/pkg/net/http/cgi/host.go
@@ -223,6 +223,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	linebody := bufio.NewReaderSize(stdoutRead, 1024)
 	headers := make(http.Header)
 	statusCode := 0
+	headerLines := 0
+	sawBlankLine := false
 	for {
 		line, isPrefix, err := linebody.ReadLine()
 		if isPrefix {
@@ -239,8 +241,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 			return
 		}
 		if len(line) == 0 {
+			sawBlankLine = true
 			break
 		}
+		headerLines++
 		parts := strings.SplitN(string(line), ":", 2)
 		if len(parts) < 2 {
 			h.printf("cgi: bogus header line: %s", string(line))
@@ -266,6 +270,11 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 			headers.Add(header, val)
 		}
 	}
+	if headerLines == 0 || !sawBlankLine {
+		rw.WriteHeader(http.StatusInternalServerError)
+		h.printf("cgi: no headers")
+		return
+	}
 
 	if loc := headers.Get("Location"); loc != "" {
 		if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
@@ -277,6 +286,12 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		}
 	}
 
+	if statusCode == 0 && headers.Get("Content-Type") == "" {
+		rw.WriteHeader(http.StatusInternalServerError)
+		h.printf("cgi: missing required Content-Type in headers")
+		return
+	}
+
 	if statusCode == 0 {
 		statusCode = http.StatusOK
 	}

src/pkg/net/http/cgi/matryoshka_test.go

--- a/src/pkg/net/http/cgi/matryoshka_test.go
+++ b/src/pkg/net/http/cgi/matryoshka_test.go
@@ -128,6 +128,7 @@ func TestKillChildAfterCopyError(t *testing.T) {
 }
 
 // Test that a child handler writing only headers works.
+// golang.org/issue/7196
 func TestChildOnlyHeaders(t *testing.T) {
 	h := &Handler{
 		Path: os.Args[0],
@@ -143,6 +144,26 @@ func TestChildOnlyHeaders(t *testing.T) {
 	}
 }
 
+// golang.org/issue/7198
+func Test500WithNoHeaders(t *testing.T)     { want500Test(t, "/immediate-disconnect") }
+func Test500WithNoContentType(t *testing.T) { want500Test(t, "/no-content-type") }
+func Test500WithEmptyHeaders(t *testing.T)  { want500Test(t, "/empty-headers") }
+
+func want500Test(t *testing.T, path string) {
+	h := &Handler{
+		Path: os.Args[0],
+		Root: "/test.go",
+		Args: []string{"-test.run=TestBeChildCGIProcess"},
+	}
+	expectedMap := map[string]string{
+		"_body": "",
+	}
+	replay := runCgiTest(t, h, "GET "+path+" HTTP/1.0\\nHost: example.com\\n\\n", expectedMap)
+	if replay.Code != 500 {
+		t.Errorf("Got code %d; want 500", replay.Code)
+	}
+}
+
 type neverEnding byte
 
 func (b neverEnding) Read(p []byte) (n int, err error) {
@@ -158,6 +179,16 @@ func TestBeChildCGIProcess(t *testing.T) {
 		// Not in a CGI environment; skipping test.
 		return
 	}
+	switch os.Getenv("REQUEST_URI") {
+	case "/immediate-disconnect":
+		os.Exit(0)
+	case "/no-content-type":
+		fmt.Printf("Content-Length: 6\\n\\nHello\\n")
+		os.Exit(0)
+	case "/empty-headers":
+		fmt.Printf("\\nHello")
+		os.Exit(0)
+	}
 	Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 		rw.Header().Set("X-Test-Header", "X-Test-Value")
 		req.ParseForm()

コアとなるコードの解説

src/pkg/net/http/cgi/host.go

  • headerLinessawBlankLineの導入: CGIスクリプトからの出力は、まずHTTPヘッダーが複数行続き、その後に空行が来て、最後にメッセージボディが続くという形式です。headerLinesは、このヘッダー部分で何行読み込んだかを追跡します。sawBlankLineは、ヘッダーとボディを区切る重要な空行が検出されたかどうかを記録します。これらの変数は、CGIレスポンスがRFC 3875の規定に準拠しているかを判断するための状態を保持します。

  • 不正なヘッダーブロックのチェック: if headerLines == 0 || !sawBlankLineの条件は、CGIスクリプトがヘッダーを全く返さなかった場合(headerLines == 0)や、ヘッダーの後に空行がなかった場合(!sawBlankLine)を検出します。RFC 3875では、ヘッダーブロックは少なくとも1つのヘッダー行とそれに続く空行で構成される必要があるため、これらのケースは不正なレスポンスとみなされます。この場合、http.StatusInternalServerError (500) をクライアントに返し、エラーメッセージをログに出力して処理を中断します。これにより、サーバーはCGIスクリプトの出力が不完全であることを明確に示します。

  • Content-Typeヘッダーのチェック: if statusCode == 0 && headers.Get("Content-Type") == ""の条件は、CGIスクリプトがHTTPステータスコードを明示的に設定せず(statusCode == 0はデフォルトの200 OKが適用される前)、かつContent-Typeヘッダーが欠落している場合に発動します。RFC 3875では、CGIスクリプトがメッセージボディを返す場合、Content-Typeヘッダーは必須とされています。この条件が真の場合も、http.StatusInternalServerError (500) を返し、エラーメッセージをログに出力します。これは、CGIスクリプトが必須ヘッダーを提供しなかったことを示し、クライアントがレスポンスボディを正しく解釈できない可能性を回避します。

src/pkg/net/http/cgi/matryoshka_test.go

  • Test500WithNoHeaders, Test500WithNoContentType, Test500WithEmptyHeaders: これらのテスト関数は、host.goで追加された不正なCGIレスポンスの検出ロジックを検証するために導入されました。それぞれ、CGIスクリプトが「ヘッダーなしで即座に終了する」「Content-Typeなしでレスポンスを返す」「空行のみを返す」という3つの異なる不正なシナリオをシミュレートします。

  • want500Testヘルパー関数: この関数は、上記3つのテスト関数で共通のテストロジックをカプセル化しています。CGIハンドラを設定し、特定のパスへのリクエストを実行し、その結果としてHTTP 500ステータスコードが返されることをアサートします。これにより、テストコードの重複が避けられ、可読性が向上します。

  • TestBeChildCGIProcess内のswitch os.Getenv("REQUEST_URI"): このswitch文は、テスト中にCGI子プロセスとして実行される際に、REQUEST_URI環境変数の値に基づいて異なる不正なCGIレスポンスを生成するためのものです。これにより、各テストケースが意図する不正なCGIスクリプトの挙動を正確にシミュレートできます。例えば、/immediate-disconnectの場合は何も出力せずに終了し、/no-content-typeの場合はContent-Typeヘッダーを省略したレスポンスを生成します。

これらの変更により、Goのnet/http/cgiパッケージは、RFC 3875の仕様に厳密に準拠し、CGI子プロセスからの不正なレスポンスに対してより堅牢なエラーハンドリングを提供するようになりました。

関連リンク

参考にした情報源リンク