[インデックス 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トラフィックをプロキシ経由でルーティングする際に利用されます。
- 動作原理:
- クライアントはプロキシに
CONNECT target_host:target_port HTTP/1.1の形式でリクエストを送信します。 - プロキシは
target_host:target_portへのTCP接続を試みます。 - 接続が成功した場合、プロキシはクライアントに
HTTP/1.1 200 Connection Establishedを返します。 - その後、プロキシはクライアントとターゲットサーバー間の生データを双方向に中継する「トンネル」として機能します。プロキシはトンネル内のデータを検査したり変更したりしません。
- クライアントはプロキシに
- 主な用途:
- HTTPSプロキシ: 最も一般的な用途です。クライアントとHTTPSサーバー間のTLSハンドシェイクをプロキシを介して直接行い、エンドツーエンドの暗号化を維持します。
- その他のプロトコルのトンネリング: SSHやVPNなど、HTTP以外のTCPベースのプロトコルをHTTPプロキシ経由でトンネリングするためにも使用できます。
- 特徴:
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/rpc が CONNECT を使用する場合、そのリクエスト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つの主要な使用パターンがあります。
- 標準的なプロキシトンネリング:
CONNECT www.google.com:443 HTTP/1.1- この場合、
Request-URIはwww.google.com:443であり、これはURLの「authority」(ホストとポート)部分に相当します。これをurl.URL構造体のHostフィールドに格納する必要があります。
- この場合、
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.ParseRequestはhost: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つのファイルが変更されています。
src/pkg/net/http/readrequest_test.go:CONNECTリクエストのパースをテストするための新しいテストケースが追加されています。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つの新しいテストケースが追加されています。
CONNECT www.google.com:443 HTTP/1.1:- これは標準的なプロキシトンネリングのケースです。
- 期待される
Requestオブジェクトでは、Methodが"CONNECT"、URL.Hostが"www.google.com:443"、Hostも"www.google.com:443"となることがテストされています。URL.Pathは空です。
CONNECT 127.0.0.1:6060 HTTP/1.1:- IPアドレスとポートを指定するケースです。
- 同様に、
URL.HostとHostが"127.0.0.1:6060"となることがテストされています。
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を識別するための条件です。- この
justAuthorityがtrueの場合、リクエストURIはhost:port形式であると判断されます。
rawurl = "http://" + rawurl:justAuthorityがtrueの場合、rawurlの先頭に一時的に"http://"スキーマを付加します。- これは、
url.ParseRequest関数がhost:portのみを渡された場合に正しくパースできない可能性があるため、有効なURL形式に一時的に変換するためのトリックです。これにより、url.ParseRequestはhost:port部分をURL.Hostフィールドに正しくマッピングできるようになります。
req.URL, err = url.ParseRequest(rawurl):- 変更された(または変更されていない)
rawurlをurl.ParseRequestに渡してパースします。
- 変更された(または変更されていない)
if justAuthority { req.URL.Scheme = "" }:justAuthorityがtrueであった場合(つまり、一時的に"http://"を付加した場合)、パース後にreq.URL.Schemeを空文字列に戻します。- これは、
CONNECTリクエストが本来スキーマを持たないため、パースのために付加したスキーマを元に戻すことで、RequestオブジェクトがCONNECTリクエストの特性を正しく反映するようにするためです。
この一連のロジックにより、ReadRequest 関数は CONNECT リクエストの多様な形式を適切に処理し、Request オブジェクトの URL フィールドを正確に設定できるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/c3b9650caa7715c8961dcb5d7503b90b6dbae7cb
- Go CL (Code Review): https://golang.org/cl/5571052
- 関連するIssue: https://golang.org/issue/2755
参考にした情報源リンク
- 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