2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ProxyConnectHeaderを使ってhttp.ClientによるCONNECTメソッドにヘッダーを付加する

Posted at

事象

Go 1.14.2の環境で、認証Proxyを経由して任意のサイトにアクセスするHTTPクライアントを実装していたところ、接続先がHTTPSのときに認証proxyから407が返ってしまいました。

再現コード(main.go)
func main() {
	u, _ := url.Parse("http://127.0.0.1:3128") // ローカルにsquidを立て、認証Proxyとした
	req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Proxy-Authorization", "Basic <basic auth string>")
	c := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(u),
		},
	}
	resp, err := c.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status)
}

上記を実行すると、 407 (Proxy Authentication Required) が返って通信ができません。 Proxy-Authorization に然るべき認証情報は正しくセットしているのですが、なぜ通信できないのでしょうか。

$ go run main.go
2020/08/09 16:02:34 Get https://www.yahoo.co.jp/: Proxy Authentication Required
exit status 1

原因

接続先がHTTPSの場合、接続先とのTLSハンドシェイクの前に、仲介者である認証Proxyに対して CONNECT メソッドが発行されます。再現コードでヘッダーとして付与していた Proxy-Authorization が、このCONNECTメソッド実行時に付与されなかったため、本事象が発生していました。

CONNECTの実行は、内部的には http.TransportdialConn メソッドにて実行されています。このメソッドでは、CONNECTを行うために http.Request を新たに生成していますが、ここではもともと再現コードでProxy-AuthorizationをセットしたHeaderを使いません。その代わり、 http.TransportProxyConnectHeader という項目の値をセットしています。

http.Transport#dialConnより抜粋(transport.go#L1564-L1579)
	case cm.targetScheme == "https":
		conn := pconn.conn
		hdr := t.ProxyConnectHeader
		if hdr == nil {
			hdr = make(Header)
		}
		if pa := cm.proxyAuth(); pa != "" {
			hdr = hdr.Clone()
			hdr.Set("Proxy-Authorization", pa)
		}
		connectReq := &Request{
			Method: "CONNECT",
			URL:    &url.URL{Opaque: cm.targetAddr},
			Host:   cm.targetAddr,
			Header: hdr,
		}

このCONNECT時に元のHeaderを使わない仕様、私は最初ちょっと意外に感じたのですが、考えてみれば、元々の要求でセットしていたHeaderはあくまで本来の接続先のために用意しているものなので、認証Proxyとの会話であるCONNECT時にそれを渡してはならないですね。不要ですし、何よりセキュリティの観点から望ましくないです。

ProxyConnectHeader は、当然ながら http.Transport のコメントでも説明されています。


    // ProxyConnectHeader optionally specifies headers to send to
    // proxies during CONNECT requests.
    ProxyConnectHeader Header // Go 1.8

対応

http.Request.Header にセットしていた値を、 ProxyConnectHeader に入れてあげるよう変更すれば、事象の解消が可能です。

main.go
func main() {
	u, _ := url.Parse("http://127.0.0.1:3128")
	req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil)
	if err != nil {
		log.Fatal(err)
	}
	hdr := make(http.Header)
	hdr.Add("Proxy-Authorization", "Basic <basic auth string>")
	c := &http.Client{
		Transport: &http.Transport{
			Proxy:              http.ProxyURL(u),
			ProxyConnectHeader: hdr,
		},
	}
	resp, err := c.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status)
}

いけましたね。

$ go run main.go
200 OK

また、Proxy-Authorization に固定の認証情報をセットしたいだけなら、ProxyのURLに入れても良いですね。

func main() {
	u, _ := url.Parse("http://<username>:<password>@127.0.0.1:3128") // 認証情報を付加
	req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil)
	if err != nil {
		log.Fatal(err)
	}
	c := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(u),
		},
	}
	resp, err := c.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status)
}
$ go run main.go
200 OK

おわり。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?