[インデックス 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