go net/http/httptest:使使用服务器(无论是否为TLS)测试cookies成为可能

wpx232ag  于 4个月前  发布在  Go
关注(0)|答案(8)|浏览(55)

httptest.NewTLSServer 使用了一个对本地主机无效的证书

你正在使用的 Go 版本是什么( go version )?

$ go version
go version go1.12.1 darwin/amd64

这个问题在最新版本中是否重现?

是的。

你正在使用什么操作系统和处理器架构( go env )?

go env 输出

$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/danielcormier/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/danielcormier/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/Cellar/go/1.12.1/libexec"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.12.1/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/84/_6l41bt970l9fmsrwc_p1gv00000gn/T/go-build114647702=/tmp/go-build -gno-record-gcc-switches -fno-common"

你做了什么?

我正在尝试测试一些涉及设置 Domain 属性的 cookie 的内容。如在 #12610 中讨论的,*net/http/cookiejar.Jar 不会为 IP 返回 cookie(根据相关的 RFCs)。(*httptest.Server).URL 将主机设置为 IP 地址(默认为 127.0.0.1::1 )。
为了完成所需的测试,我使用 httptest.NewTLSServer(...) 启动了一个 *httptest.Server,将 (*httptest.Server).URL 中的 IP 替换为 localhost,并尝试向其发送请求。

你期望看到什么?

我期望 httptest.NewTLSServer(...) 能够使用一个对 localhost 以及回环 IP 地址有效的 TLS 证书。
我期望能够成功地使用 (*httptest.Server).Client()localhost 发起 HTTPS 请求,该请求应发送到 *httptest.Server 正在监听的正确端口。

你看到了什么?

x509: certificate is valid for example.com, not localhost

示例

为了完整性,我包括了一组测试,展示了在使用 *cookiejar.Jar 和各种 *httptest.Server 组合时的不同行为。这里出现问题的测试是 TestCookies/tls/localhost/default_cert (第 174 行)。第 185 行的测试表明,如果我向 localhost 发送带有对该主机名有效证书的请求,原始的 cookie 问题就会得到解决。

package cookies_test

import (
	"crypto/tls"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"
)

func TestCookies(t *testing.T) {
	const (
		routeSetCookie    = "/set-cookie"
		routeExpectCookie = "/expect-cookie"

		cookieName = "token"
	)

	handler := func(tb testing.TB) http.Handler {
		setCookie := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			host, _, err := net.SplitHostPort(r.Host)
			if err != nil {
				host = r.Host
			}

			cookie := &http.Cookie{
				Name:     cookieName,
				Value:    "the value",
				Domain:   host,
				Path:     "/",
				HttpOnly: true,
			}

			tb.Logf("Setting cookie: %s", cookie)

			http.SetCookie(w, cookie)
		})

		expectCookie := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			_, err := r.Cookie(cookieName)
			switch err {
			case nil:
				// Success!

			case http.ErrNoCookie:
				msg := "No cookie"
				tb.Log(msg)
				http.Error(w, msg, http.StatusBadRequest)
				return

			default:
				msg := fmt.Sprintf("Failed to get cookie: %v", err)
				tb.Log(msg)
				http.Error(w, msg, http.StatusInternalServerError)
				return
			}

			tb.Log("The cookie was set")
		})

		mux := http.NewServeMux()
		mux.Handle(routeSetCookie, setCookie)
		mux.Handle(routeExpectCookie, expectCookie)

		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			target := r.RequestURI
			tb.Logf("---- START %s", target)
			mux.ServeHTTP(w, r)
			tb.Logf("---- END %s", target)
		})
	}

	testCookies := func(tb testing.TB, svr *httptest.Server) {
		jar, err := cookiejar.New(nil)
		if err != nil {
			tb.Fatal(err)
		}

		httpClient := svr.Client()
		httpClient.Timeout = 1 * time.Second
		httpClient.Jar = jar

		resp, err := httpClient.Get(svr.URL + routeExpectCookie)
		if err != nil {
			tb.Fatal(err)
		}

		defer func() {
			io.Copy(ioutil.Discard, resp.Body)
			resp.Body.Close()
		}()

		if resp.StatusCode != http.StatusBadRequest {
			body, _ := ioutil.ReadAll(resp.Body)
			tb.Fatalf("Should not have cookie: %s\n%s", resp.Status, body)
		}

		resp, err = httpClient.Get(svr.URL + routeSetCookie)
		if err != nil {
			tb.Fatal(err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			tb.Fatalf("%s\n%s", resp.Status, body)
		}

		resp, err = httpClient.Get(svr.URL + routeExpectCookie)
		if err != nil {
			tb.Fatal(err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			tb.Fatalf("%s\n%s", resp.Status, body)
		}
	}

	useLocalhost := func(tb testing.TB, svr *httptest.Server) {
		svrURL, err := url.Parse(svr.URL)
		if err != nil {
			tb.Fatal(err)
		}

		svrURL.Host = net.JoinHostPort("localhost", svrURL.Port())

		svr.URL = svrURL.String()
	}

	t.Run("no tls", func(t *testing.T) {
		t.Run("ip", func(t *testing.T) {
			// This fails because `cookiejar.CookieJar` follows the RFCs and drops the `Domain` of a
			// cookie where its set to an IP, rather than a domain. We'll skip it, but it's here if
			// you want to see for yourself.
			t.SkipNow()

			svr := httptest.NewServer(handler(t))
			defer svr.Close()

			testCookies(t, svr)
		})

		t.Run("localhost", func(t *testing.T) {
			// This works.

			svr := httptest.NewServer(handler(t))
			defer svr.Close()

			useLocalhost(t, svr)
			testCookies(t, svr)
		})
	})

	t.Run("tls", func(t *testing.T) {
		t.Run("ip", func(t *testing.T) {
			// This fails because `cookiejar.CookieJar` follows the RFCs and drops the `Domain` of a
			// cookie where its set to an IP, rather than a domain. We'll skip it, but it's here if
			// you want to see for yourself.
			t.SkipNow()

			svr := httptest.NewTLSServer(handler(t))
			defer svr.Close()

			testCookies(t, svr)
		})

		t.Run("localhost", func(t *testing.T) {
			t.Run("default cert", func(t *testing.T) {
				// This fails because the cert `httptest.NewTLSServer` serves up is valid for
				// 127.0.0.1, ::1, and examlple.com. Not localhost.

				svr := httptest.NewTLSServer(handler(t))
				defer svr.Close()

				useLocalhost(t, svr)
				testCookies(t, svr)
			})

			t.Run("localhost cert", func(t *testing.T) {
				// This works. But, you need to generate your own cert for localhost.

				svr := httptest.NewUnstartedServer(handler(t))

				certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDJTCCAg2gAwIBAgIQas3l/GRJkOGvAZP1CLIvRzANBgkqhkiG9w0BAQsFADAb
MRkwFwYDVQQKExBBY21lIENvcnBvcmF0aW9uMCAXDTAwMDEwMTAwMDAwMFoYDzIx
MDAwMTAxMDAwMDAwWjAbMRkwFwYDVQQKExBBY21lIENvcnBvcmF0aW9uMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn46okXbPHDmuwHMcQHyPtDl1qoKL
WA/U5x1VcLHGOR6vQKjkNUXbW0yU0HYcyBtmr5gugdmlaFvCRlMSaG1pyC5iCCha
HlTyFyaZi0o2zGT34fS8Jj2WUKE/pR9pOqEoWx8UezBHw/NBZjGCjKe4ASzCQqbn
KA6DxQfRBypU+OFAIK3KsRP6Xvwqd2N/a5FybL9ixKYNbAj7b2vAhW7NIWw++m2T
Hif+bTsEhLAGUG3KGW9OGcJiAewyZb4DgZPgE1ourEud9goVbcCTZBYbpV3U0tZa
XxqJIOfhsfCe4fDcqe1Hspq47SLdvP8FP/qKTbFOqoA/NAlrmboxOw+mXwIDAQAB
o2MwYTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0T
AQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAAAAAAAAAAAAAAAAAAAAGH
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAG2cVK1TZvHoaiqA40QEjKehKqq4vKLc
At/FrITEgvNTIkvguEvLw5wsUO/3Nt/atjWtFdSJCLWCLzrgiLOLtJubkrDzzbus
/OsI0cf/fMTyCnjt64efSz2RDPPllRbJd3zZBkuOWhPoxx/Sz0VRvQKGFb9mvPoI
PTZ22ugwZdS/3PnMEoVO46iQumGARXQEbiGApeXPObK0E6Fs7pqwomU9Ny2XsyXS
je06pfouDv8UlLzZLY/fVJLHN6aM7odw5iPp2p7ttFdgn1l/LVlZVX9FBwegHXet
5OSC7pDc+kbLg1cJE8/7dF47VBEVKSvr5ldgRuvDtEf4PupKRl4rhik=
-----END CERTIFICATE-----`)

				keyPEM := []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAn46okXbPHDmuwHMcQHyPtDl1qoKLWA/U5x1VcLHGOR6vQKjk
NUXbW0yU0HYcyBtmr5gugdmlaFvCRlMSaG1pyC5iCChaHlTyFyaZi0o2zGT34fS8
Jj2WUKE/pR9pOqEoWx8UezBHw/NBZjGCjKe4ASzCQqbnKA6DxQfRBypU+OFAIK3K
sRP6Xvwqd2N/a5FybL9ixKYNbAj7b2vAhW7NIWw++m2THif+bTsEhLAGUG3KGW9O
GcJiAewyZb4DgZPgE1ourEud9goVbcCTZBYbpV3U0tZaXxqJIOfhsfCe4fDcqe1H
spq47SLdvP8FP/qKTbFOqoA/NAlrmboxOw+mXwIDAQABAoIBAQCLVxlNF5WdT56W
ALDOfDk/KeLhSmoIOKM0RkDETuwOHAbuj8/j2iLLo6BeQJe4BX3yoRMUYQ77iQ6r
PYbY3ZxAroj8GMlCrepRX2s94kziyNZVZNYfCy/HMFqViE3sXqsQkJ7hSfOSY1Bc
v6YD0cB2fjET5g/+wlY+7imUeVqFkUd5+CuSa4MVheWRiXCFydm+GdUMbHGJauZk
KYSz6oE5vXkDCbcjpyH2Ay7QuHiE00wI2DqsvkZJy8et+XgYL8iNj10JulnDXJg6
MmSf0ZsDfhfJW9AQDzZjXfbSRsskztnehN4UcJH8enLaLbanlYisPpIsj9jpqLwt
EDcsHX+ZAoGBAMC0nO9MSoxu4Q9WP8Lq5qT1QYrRvmSO9mHz+4wmYvConofucqlK
M6HXD/qXIU8pTHZ5WHjnnEyNOvVdsK/P6uYkdqWRXig8/zgoi6DGuujlthJ7BKYW
I7Fvh2z217p3y0IHQvHYjxQk0ag9kOxkdqiYW6WxNcUj2QeXgDkEjcddAoGBANP2
0XI1tEm+IThXHnnT8DYci4xIi9JBvkWgq+atw8ZNicNfN+a0RX5f42fFUCkGE3F6
JgQgSwIAr6zbLKH8RzwU/V5dpO7vuPrgsCRwFsovKAhyCpW0PflJXIKPY6xrbRnc
t2cSOitZzWBdGQJQANGcd+qdGDG/NBcsYdchKfTrAoGAMS/ovsviW2YR3DBPphj/
NivDxwMybchv6yCznFpP9s2TaW7bpYpjE3Qph/T7c5E/Cx5+Dp5Prtp9qhN3/eg8
NPIptqkcN3kaS+NNgIQ5QSkhCCaOUTZldezZzF5VQitBnmDsHX8BRkr/mMneK/iY
sP/ypKBO8TrtMprhB6y546ECgYEAgFXwejYJ8pwrgPE+goTP6/NcipNiFOu5SG7/
pauP3YEU6DW+ovCDIwDrrujIoA4Nt6c9XUIwKAZCV2Zcn7cfakFLJteMBR8f4MYp
3+X95mym0HY78mgvHcBNQr+OmdZxODdq0/01OwokTzQO8FeAJ2mVMXfsLjKWV3GH
y7lIrgECgYEAiZIEx3fBc3TBcaZb5vbWyAQyfC5vgI0N25ZaIwoG5g6CkjKt8nfH
Xfl1da9pWbcAgRLlq+XhqAJQdUjZ0NfKeWSQxT8TQob8ZfiAHXwjTf20qFrarsPl
jVyKqKuj7Vl7evexIhY03RL6S/koyDGJWdUt9myZB6mdFJBBFQIuv8U=
-----END RSA PRIVATE KEY-----`)

				cert, err := tls.X509KeyPair(certPEM, keyPEM)
				if err != nil {
					t.Fatal(err)
				}

				svr.TLS = &tls.Config{
					Certificates: []tls.Certificate{cert},
				}

				svr.StartTLS()
				defer svr.Close()

				useLocalhost(t, svr)
				testCookies(t, svr)
			})
		})
	})
}

我曾尝试让 a conversation 关于 golang-nuts 组中使用的 httptest.NewTLSServer() 证书所包含的主机进行讨论,但没有取得任何进展。

6ljaweal

6ljaweal1#

这是一个向公众开放的关键对。即,它的私有部分是众所周知的。在本地主机或环回接口上使其匹配将是一个巨大的安全漏洞,数百万的开发人员将其添加到受信任的证书存储中,然后他们的机器将容易受到通过本地主机中间人攻击的广泛类别的威胁。
请按照 howto 操作并为自己创建一个证书。
请注意页面下方的 minica 链接。

2o7dmzc5

2o7dmzc52#

将它与本地主机或环回接口匹配,对于数百万开发者来说,这是一个巨大的安全漏洞。他们会将其添加到受信任的证书存储中,然后他们的机器就会容易受到通过本地主机中间人攻击的广泛类别威胁。
它已经匹配了IPv4和IPv6环回地址。
出于你提到的原因,从一个众所周知的密钥对中添加一个证书到受信任的证书是不明智的。更安全的做法是自己生成并信任。

dphi5xsq

dphi5xsq4#

Dup of #30774,似乎?这是不支持的。httptest.NewTLSServer用于测试,您只能使用它提供的URL访问它。

6yt4nkrj

6yt4nkrj5#

Dup of #30774 ,似乎?这是不支持的。httptest.NewTLSServer用于测试,您只能使用它提供的URL访问它。
然后这是一个功能请求,我猜。目标是使使用httptest.NewTLSServer进行测试更容易。

6vl6ewon

6vl6ewon6#

功能请求SGTM。如果返回的客户端有一个特殊拨号器,可以针对某个名称("*.test.example")拨打该IP,并使TLS正常工作,我没问题。
有人想为此做贡献吗?

kmbjn2e3

kmbjn2e37#

任何人想参与吗?
我对这个感兴趣。

0g0grzrc

0g0grzrc8#

https://golang.org/cl/182917提到了这个问题:net/http/httptest: make it possible to use a Server (TLS or not) to test cookies

相关问题