事象
Go 1.14.2の環境で、認証Proxyを経由して任意のサイトにアクセスするHTTPクライアントを実装していたところ、接続先がHTTPSのときに認証proxyから407が返ってしまいました。
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.Transport
の dialConn
メソッドにて実行されています。このメソッドでは、CONNECTを行うために http.Request
を新たに生成していますが、ここではもともと再現コードでProxy-AuthorizationをセットしたHeaderを使いません。その代わり、 http.Transport
の ProxyConnectHeader
という項目の値をセットしています。
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
に入れてあげるよう変更すれば、事象の解消が可能です。
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
おわり。