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

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

このコミットは、Go言語の net/http パッケージにおいて、HTTP CONNECT メソッドのリクエストのパース処理を改善するものです。特に、プロキシトンネリングに使用される CONNECT host:port 形式と、net/rpc パッケージで使用される CONNECT /path 形式の両方を正しく処理できるようにするための変更が含まれています。

コミット

commit c3b9650caa7715c8961dcb5d7503b90b6dbae7cb
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Jan 25 11:42:00 2012 +1100

    net/http: parse CONNECT requests
    
    Fixes #2755
    
    R=dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/5571052

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

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

元コミット内容

net/http パッケージが CONNECT リクエストを正しくパースできるようにする。これはIssue #2755を修正するものである。

変更の背景

HTTP CONNECT メソッドは、主にプロキシサーバーを介してTCPトンネルを確立するために使用されます。最も一般的なユースケースは、HTTPSトラフィックをプロキシ経由でトンネリングすることです。クライアントはプロキシに対して CONNECT host:port HTTP/1.1 の形式でリクエストを送信し、プロキシは指定された host:port へのTCP接続を確立し、その後のクライアントとターゲットサーバー間の生データを中継します。これにより、プロキシは暗号化されたHTTPSトラフィックの内容を検査することなく、安全なエンドツーエンドの通信を可能にします。

しかし、Go言語の net/rpc パッケージも、RPC通信のために CONNECT メソッドを使用する場合があります。この場合、CONNECT リクエストのターゲットは host:port 形式ではなく、/path 形式(例: CONNECT /_goRPC_ HTTP/1.1)となります。

従来の net/http パッケージの ReadRequest 関数は、これらの異なる CONNECT リクエストの形式を適切に区別してパースすることができませんでした。特に、CONNECT メソッドの後に host:port が続く場合、それをURLとして正しく解釈し、Request.URL.Host に格納する必要がありました。また、net/rpc のようにパスが続く場合は、Request.URL.Path に格納されるべきでした。このパースの不整合が、特定のシナリオで問題を引き起こしていました(Issue #2755)。

このコミットは、net/http パッケージが CONNECT リクエストのこれらの二つの異なる使用方法を認識し、それぞれを Request オブジェクトの適切なフィールド(URL.Host または URL.Path)に正確にマッピングできるようにすることで、この問題を解決することを目的としています。

前提知識の解説

HTTP CONNECT メソッド

HTTP CONNECT メソッドは、HTTPプロキシサーバーに対して、指定された宛先へのTCP接続を確立し、その接続をクライアントにトンネリングするように要求するために使用されます。これは、主にHTTPSトラフィックをプロキシ経由でルーティングする際に利用されます。

  1. 動作原理:
    • クライアントはプロキシに CONNECT target_host:target_port HTTP/1.1 の形式でリクエストを送信します。
    • プロキシは target_host:target_port へのTCP接続を試みます。
    • 接続が成功した場合、プロキシはクライアントに HTTP/1.1 200 Connection Established を返します。
    • その後、プロキシはクライアントとターゲットサーバー間の生データを双方向に中継する「トンネル」として機能します。プロキシはトンネル内のデータを検査したり変更したりしません。
  2. 主な用途:
    • HTTPSプロキシ: 最も一般的な用途です。クライアントとHTTPSサーバー間のTLSハンドシェイクをプロキシを介して直接行い、エンドツーエンドの暗号化を維持します。
    • その他のプロトコルのトンネリング: SSHやVPNなど、HTTP以外のTCPベースのプロトコルをHTTPプロキシ経由でトンネリングするためにも使用できます。
  3. 特徴:
    • CONNECT リクエストのURIは、通常、スキーマやパスを含まず、host:port の形式を取ります。
    • CONNECT は、他のHTTPメソッド(GET, POSTなど)とは異なり、プロキシがHTTPメッセージの内容を処理するのではなく、単にTCP接続を中継する点が異なります。

Go言語の net/rpc パッケージと CONNECT

Go言語の標準ライブラリである net/rpc パッケージは、Goプログラム間でRPC(Remote Procedure Call)を実装するためのメカニズムを提供します。net/rpc は、HTTPプロトコルをトランスポートとして使用するオプションも持っており、その際に CONNECT メソッドを利用することがあります。

net/rpcCONNECT を使用する場合、そのリクエストURIは通常のプロキシトンネリングとは異なり、/_goRPC_ のようなパス形式を取ります。これは、net/rpc がプロキシを介したトンネリングではなく、HTTPサーバー上の特定のパスを介してRPCサービスに接続するために CONNECT を利用しているためです。このため、net/http パッケージは、CONNECT リクエストが host:port 形式であるか、/path 形式であるかを区別して処理する必要がありました。

技術的詳細

このコミットの技術的な核心は、net/http パッケージの ReadRequest 関数が、HTTP CONNECT メソッドのリクエストラインをどのようにパースするかを改善することにあります。

HTTPリクエストの最初の行は「リクエストライン」と呼ばれ、Method Request-URI HTTP-Version の形式を取ります。CONNECT メソッドの場合、Request-URI の部分が問題となります。

従来の ReadRequest 関数は、Request-URI を常に完全なURLとしてパースしようとしていました。しかし、CONNECT メソッドには以下の2つの主要な使用パターンがあります。

  1. 標準的なプロキシトンネリング: CONNECT www.google.com:443 HTTP/1.1
    • この場合、Request-URIwww.google.com:443 であり、これはURLの「authority」(ホストとポート)部分に相当します。これを url.URL 構造体の Host フィールドに格納する必要があります。
  2. net/rpc の使用: CONNECT /_goRPC_ HTTP/1.1
    • この場合、Request-URI/_goRPC_ であり、これはURLの「path」部分に相当します。これを url.URL 構造体の Path フィールドに格納する必要があります。

問題は、url.ParseRequest 関数が、www.google.com:443 のような文字列を単独でパースしようとすると、それが有効なURLとして認識されない可能性がある点です。特に、スキーマ(http:// など)がないため、パースエラーになるか、意図しない結果になる可能性があります。

このコミットでは、この問題を解決するために以下のロジックが導入されました。

  • ReadRequest 関数内で、リクエストメソッドが CONNECT であり、かつ Request-URI がスラッシュ(/)で始まらない場合(つまり、host:port 形式であると推測される場合)、一時的に Request-URI の前に http:// スキーマを付加します。
  • この変更された文字列を url.ParseRequest に渡してパースさせます。http:// を付加することで、url.ParseRequesthost:port 部分を URL.Host フィールドに正しく格納できるようになります。
  • パースが完了した後、CONNECT リクエストの本来の意図に合わせて、一時的に付加した http:// スキーマを req.URL.Scheme から削除します。これにより、req.URL.Scheme は空になり、CONNECT リクエストの特性が維持されます。

この処理により、CONNECT リクエストが host:port 形式であっても /path 形式であっても、Request オブジェクトの URL フィールドが正しく設定されるようになります。特に、host:port 形式の場合は req.URL.Host に、/path 形式の場合は req.URL.Path にそれぞれ適切な値が格納されるようになります。

また、Request.write メソッドにも小さな変更が加えられ、CONNECT リクエストで URL.Path が空の場合(つまり host:port 形式の場合)、リクエストURIとして host のみを書き出すように調整されています。これは、CONNECT リクエストの標準的な形式に合致させるためのものです。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. src/pkg/net/http/readrequest_test.go: CONNECT リクエストのパースをテストするための新しいテストケースが追加されています。
  2. src/pkg/net/http/request.go: ReadRequest 関数と write メソッドに CONNECT リクエストのパースロジックが追加・修正されています。

src/pkg/net/http/readrequest_test.go の変更

--- a/src/pkg/net/http/readrequest_test.go
+++ b/src/pkg/net/http/readrequest_test.go
@@ -171,6 +171,75 @@ var reqTests = []reqTest{\n 		},\n 		noError,\n 	},\n+\n+\t// CONNECT request with domain name:\n+\t{\n+\t\t"CONNECT www.google.com:443 HTTP/1.1\\r\\n\\r\\n",\n+\n+\t\t&Request{\n+\t\t\tMethod: "CONNECT",\n+\t\t\tURL: &url.URL{\n+\t\t\t\tHost: "www.google.com:443",\n+\t\t\t},\n+\t\t\tProto:         "HTTP/1.1",\n+\t\t\tProtoMajor:    1,\n+\t\t\tProtoMinor:    1,\n+\t\t\tHeader:        Header{},\n+\t\t\tClose:         false,\n+\t\t\tContentLength: 0,\n+\t\t\tHost:          "www.google.com:443",\n+\t\t},\n+\n+\t\tnoBody,\n+\t\tnoTrailer,\n+\t\tnoError,\n+\t},\n+\n+\t// CONNECT request with IP address:\n+\t{\n+\t\t"CONNECT 127.0.0.1:6060 HTTP/1.1\\r\\n\\r\\n",\n+\n+\t\t&Request{\n+\t\t\tMethod: "CONNECT",\n+\t\t\tURL: &url.URL{\n+\t\t\t\tHost: "127.0.0.1:6060",\n+\t\t\t},\n+\t\t\tProto:         "HTTP/1.1",\n+\t\t\tProtoMajor:    1,\n+\t\t\tProtoMinor:    1,\n+\t\t\tHeader:        Header{},\n+\t\t\tClose:         false,\n+\t\t\tContentLength: 0,\n+\t\t\tHost:          "127.0.0.1:6060",\n+\t\t},\n+\n+\t\tnoBody,\n+\t\tnoTrailer,\n+\t\tnoError,\n+\t},\n+\n+\t// CONNECT request for RPC:\n+\t{\n+\t\t"CONNECT /_goRPC_ HTTP/1.1\\r\\n\\r\\n",\n+\n+\t\t&Request{\n+\t\t\tMethod: "CONNECT",\n+\t\t\tURL: &url.URL{\n+\t\t\t\tPath: "/_goRPC_",\n+\t\t\t},\n+\t\t\tProto:         "HTTP/1.1",\n+\t\t\tProtoMajor:    1,\n+\t\t\tProtoMinor:    1,\n+\t\t\tHeader:        Header{},\n+\t\t\tClose:         false,\n+\t\t\tContentLength: 0,\n+\t\t\tHost:          "",\n+\t\t},\n+\n+\t\tnoBody,\n+\t\tnoTrailer,\n+\t\tnoError,\n+\t},\n }

src/pkg/net/http/request.go の変更

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -305,6 +305,9 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) err
 	ruri := req.URL.RequestURI()
 	if usingProxy && req.URL.Scheme != "" && req.URL.Opaque == "" {
 		ruri = req.URL.Scheme + "://" + host + ruri
+	} else if req.Method == "CONNECT" && req.URL.Path == "" {
+		// CONNECT requests normally give just the host and port, not a full URL.
+		ruri = host
 	}
 	// TODO(bradfitz): escape at least newlines in ruri?
 
@@ -463,10 +466,29 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
 		return nil, &badStringError{"malformed HTTP version", req.Proto}
 	}
 
+	// CONNECT requests are used two different ways, and neither uses a full URL:
+	// The standard use is to tunnel HTTPS through an HTTP proxy.
+	// It looks like "CONNECT www.google.com:443 HTTP/1.1", and the parameter is
+	// just the authority section of a URL. This information should go in req.URL.Host.
+	//
+	// The net/rpc package also uses CONNECT, but there the parameter is a path
+	// that starts with a slash. It can be parsed with the regular URL parser,
+	// and the path will end up in req.URL.Path, where it needs to be in order for
+	// RPC to work.
+	justAuthority := req.Method == "CONNECT" && !strings.HasPrefix(rawurl, "/")
+	if justAuthority {
+		rawurl = "http://" + rawurl
+	}
+
 	if req.URL, err = url.ParseRequest(rawurl); err != nil {
 		return nil, err
 	}
 
+	if justAuthority {
+		// Strip the bogus "http://" back off.
+		req.URL.Scheme = ""
+	}
+
 	// Subsequent lines: Key: value.
 	mimeHeader, err := tp.ReadMIMEHeader()
 	if err != nil {

コアとなるコードの解説

src/pkg/net/http/readrequest_test.go

このファイルでは、reqTests というテストケースのスライスに、CONNECT リクエストに関する3つの新しいテストケースが追加されています。

  1. CONNECT www.google.com:443 HTTP/1.1:
    • これは標準的なプロキシトンネリングのケースです。
    • 期待される Request オブジェクトでは、Method"CONNECT"URL.Host"www.google.com:443"Host"www.google.com:443" となることがテストされています。URL.Path は空です。
  2. CONNECT 127.0.0.1:6060 HTTP/1.1:
    • IPアドレスとポートを指定するケースです。
    • 同様に、URL.HostHost"127.0.0.1:6060" となることがテストされています。
  3. CONNECT /_goRPC_ HTTP/1.1:
    • これは net/rpc が使用するパス形式のケースです。
    • 期待される Request オブジェクトでは、Method"CONNECT"URL.Path"/_goRPC_" となることがテストされています。この場合、URL.Host は空です。

これらのテストケースは、ReadRequest 関数が CONNECT リクエストの異なる形式を正確にパースし、Request 構造体の適切なフィールドにデータを格納できることを保証します。

src/pkg/net/http/request.go

func (req *Request) write(...) メソッドの変更

このメソッドは、Request オブジェクトをHTTPリクエストとして書き出す際に、リクエストURIを構築する部分です。

	} else if req.Method == "CONNECT" && req.URL.Path == "" {
		// CONNECT requests normally give just the host and port, not a full URL.
		ruri = host
	}
  • req.Method == "CONNECT" かつ req.URL.Path == "" の条件は、リクエストが CONNECT メソッドであり、かつ URL にパス情報が含まれていない(つまり、host:port 形式である)ことを意味します。
  • この場合、リクエストURI (ruri) は host の値に設定されます。これは、CONNECT リクエストの標準的な形式(例: CONNECT www.google.com:443 HTTP/1.1)に合致させるための修正です。

func ReadRequest(b *bufio.Reader) (...) 関数の変更

この関数は、HTTPリクエストの最初の行(リクエストライン)を読み込み、パースして Request オブジェクトを構築する主要なロジックを含んでいます。

	// CONNECT requests are used two different ways, and neither uses a full URL:
	// The standard use is to tunnel HTTPS through an HTTP proxy.
	// It looks like "CONNECT www.google.com:443 HTTP/1.1", and the parameter is
	// just the authority section of a URL. This information should go in req.URL.Host.
	//
	// The net/rpc package also uses CONNECT, but there the parameter is a path
	// that starts with a slash. It can be parsed with the regular URL parser,
	// and the path will end up in req.URL.Path, where it needs to be in order for
	// RPC to work.
	justAuthority := req.Method == "CONNECT" && !strings.HasPrefix(rawurl, "/")
	if justAuthority {
		rawurl = "http://" + rawurl
	}

	if req.URL, err = url.ParseRequest(rawurl); err != nil {
		return nil, err
	}

	if justAuthority {
		// Strip the bogus "http://" back off.
		req.URL.Scheme = ""
	}
  • コメント: まず、CONNECT リクエストが2つの異なる方法で使用されること、そしてどちらも完全なURLを使用しないことが説明されています。標準的なプロキシトンネリング(host:port)と net/rpc の使用(/path)が具体的に挙げられています。
  • justAuthority 変数:
    • req.Method == "CONNECT": リクエストメソッドが CONNECT であることを確認します。
    • !strings.HasPrefix(rawurl, "/"): リクエストURI (rawurl) がスラッシュ(/)で始まらないことを確認します。これは、host:port 形式のリクエストURIを識別するための条件です。
    • この justAuthoritytrue の場合、リクエストURIは host:port 形式であると判断されます。
  • rawurl = "http://" + rawurl:
    • justAuthoritytrue の場合、rawurl の先頭に一時的に "http://" スキーマを付加します。
    • これは、url.ParseRequest 関数が host:port のみを渡された場合に正しくパースできない可能性があるため、有効なURL形式に一時的に変換するためのトリックです。これにより、url.ParseRequesthost:port 部分を URL.Host フィールドに正しくマッピングできるようになります。
  • req.URL, err = url.ParseRequest(rawurl):
    • 変更された(または変更されていない)rawurlurl.ParseRequest に渡してパースします。
  • if justAuthority { req.URL.Scheme = "" }:
    • justAuthoritytrue であった場合(つまり、一時的に "http://" を付加した場合)、パース後に req.URL.Scheme を空文字列に戻します。
    • これは、CONNECT リクエストが本来スキーマを持たないため、パースのために付加したスキーマを元に戻すことで、Request オブジェクトが CONNECT リクエストの特性を正しく反映するようにするためです。

この一連のロジックにより、ReadRequest 関数は CONNECT リクエストの多様な形式を適切に処理し、Request オブジェクトの URL フィールドを正確に設定できるようになりました。

関連リンク

参考にした情報源リンク

  • HTTP CONNECT method for proxy tunneling:
    • https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT
    • https://www.cloudflare.com/learning/access-management/what-is-a-connect-method/
    • https://www.tetrate.io/blog/http-connect-method-explained/
  • Go net/http CONNECT request parsing:
    • https://golangbridge.org/go-http-connect-method-proxy-tunneling/
    • https://medium.com/@mlowicki/http-proxy-in-go-lang-60a9c1047677
    • https://medium.com/@mlowicki/http-proxy-in-go-lang-part-2-https-and-websockets-60a9c1047677