在 Go 中模拟 HTTPS 响应

Mocking HTTPS responses in Go

我正在尝试为向 Web 服务发出请求的包编写测试。我 运行 遇到问题可能是因为我对 TLS 缺乏了解。

目前我的测试看起来像这样:

func TestSimple() {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(200)
        fmt.Fprintf(w, `{ "fake" : "json data here" }`)
    }))
    transport := &http.Transport{
        Proxy: func(req *http.Request) (*url.URL, error) {
            return url.Parse(server.URL)
        },
    }
    // Client is the type in my package that makes requests
    client := Client{
        c: http.Client{Transport: transport},
    }

    client.DoRequest() // ...
}

我的包有一个包变量(我希望它是一个常量..)供要查询的 Web 服务的基址使用。它是一个 https URL。我在上面创建的测试服务器是纯 HTTP,没有 TLS。

默认情况下,我的测试失败并显示错误 "tls: first record does not look like a TLS handshake."

为了让它工作,我的测试在进行查询之前将包变量更改为纯 http URL 而不是 https。

有什么办法解决这个问题吗?我可以将包变量设为常量 (https),​​然后将 http.Transport 设置为 "downgrades" 到未加密的 HTTP,或者改用 httptest.NewTLSServer() 吗?

(当我尝试使用 NewTLSServer() 我得到 "http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037")

net/http 中的大多数行为都可以模拟、扩展或更改。虽然 http.Client 是实现 HTTP 客户端语义的具体类型,但它的所有字段都是导出的并且可以自定义。

特别是 Client.Transport 字段可能会被替换,以使客户端执行从使用自定义协议(例如 ftp:// 或 file://)到直接连接到本地处理程序(不生成 HTTP 协议字节或通过网络发送任何内容)。

客户端函数,例如 http.Get,都使用导出的 http.DefaultClient 包变量(您可以修改),因此使用这些便利函数的代码 不会 ,例如,必须更改为在自定义 Client 变量上调用方法。请注意,虽然在公开可用的库中修改全局行为是不合理的,但在应用程序和测试(包括库测试)中这样做非常有用。

http://play.golang.org/p/afljO086iB 包含重写请求 URL 的自定义 http.RoundTripper,以便将其路由到本地托管的 httptest.Server,以及另一个直接通过的示例对 http.Handler 的请求以及自定义 http.ResponseWriter 实现,以便创建 http.Response。第二种方法不像第一种那样勤奋(它不会在 Response 值中填写尽可能多的字段)但效率更高,并且应该足够兼容以与大多数处理程序和客户端调用方一起工作。

上面链接的代码也包含在下面:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "net/http/httptest"
    "net/url"
    "os"
    "path"
    "strings"
)

func Handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello %s\n", path.Base(r.URL.Path))
}

func main() {
    s := httptest.NewServer(http.HandlerFunc(Handler))
    u, err := url.Parse(s.URL)
    if err != nil {
        log.Fatalln("failed to parse httptest.Server URL:", err)
    }
    http.DefaultClient.Transport = RewriteTransport{URL: u}
    resp, err := http.Get("https://google.com/path-one")
    if err != nil {
        log.Fatalln("failed to send first request:", err)
    }
    fmt.Println("[First Response]")
    resp.Write(os.Stdout)

    fmt.Print("\n", strings.Repeat("-", 80), "\n\n")

    http.DefaultClient.Transport = HandlerTransport{http.HandlerFunc(Handler)}
    resp, err = http.Get("https://google.com/path-two")
    if err != nil {
        log.Fatalln("failed to send second request:", err)
    }
    fmt.Println("[Second Response]")
    resp.Write(os.Stdout)
}

// RewriteTransport is an http.RoundTripper that rewrites requests
// using the provided URL's Scheme and Host, and its Path as a prefix.
// The Opaque field is untouched.
// If Transport is nil, http.DefaultTransport is used
type RewriteTransport struct {
    Transport http.RoundTripper
    URL       *url.URL
}

func (t RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // note that url.URL.ResolveReference doesn't work here
    // since t.u is an absolute url
    req.URL.Scheme = t.URL.Scheme
    req.URL.Host = t.URL.Host
    req.URL.Path = path.Join(t.URL.Path, req.URL.Path)
    rt := t.Transport
    if rt == nil {
        rt = http.DefaultTransport
    }
    return rt.RoundTrip(req)
}

type HandlerTransport struct{ h http.Handler }

func (t HandlerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    r, w := io.Pipe()
    resp := &http.Response{
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     make(http.Header),
        Body:       r,
        Request:    req,
    }
    ready := make(chan struct{})
    prw := &pipeResponseWriter{r, w, resp, ready}
    go func() {
        defer w.Close()
        t.h.ServeHTTP(prw, req)
    }()
    <-ready
    return resp, nil
}

type pipeResponseWriter struct {
    r     *io.PipeReader
    w     *io.PipeWriter
    resp  *http.Response
    ready chan<- struct{}
}

func (w *pipeResponseWriter) Header() http.Header {
    return w.resp.Header
}

func (w *pipeResponseWriter) Write(p []byte) (int, error) {
    if w.ready != nil {
        w.WriteHeader(http.StatusOK)
    }
    return w.w.Write(p)
}

func (w *pipeResponseWriter) WriteHeader(status int) {
    if w.ready == nil {
        // already called
        return
    }
    w.resp.StatusCode = status
    w.resp.Status = fmt.Sprintf("%d %s", status, http.StatusText(status))
    close(w.ready)
    w.ready = nil
}

您收到错误 http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037 的原因是 https 需要域名(而不是 IP 地址)。域名是分配给 SSL 证书的域名。

使用您自己的证书以 TLS 模式启动 httptest 服务器

cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
    log.Panic("bad server certs: ", err)
}
certs := []tls.Certificate{cert}

server = httptest.NewUnstartedServer(router)
server.TLS = &tls.Config{Certificates: certs}
server.StartTLS()
serverPort = ":" + strings.Split(server.URL, ":")[2] // it's always https://127.0.0.1:<port>
server.URL = "https://sub.domain.com" + serverPort

要为连接提供有效的 SSL 证书,可以选择:

  1. 不提供证书和密钥
  2. 提供自签名证书和密钥
  3. 提供真正有效的证书和密钥

无证书

如果您不提供自己的证书,则会默认加载 example.com 证书。

自签名证书

要创建测试证书,可以使用 $GOROOT/src/crypto/tls/generate_cert.go --host "*.domain.name"

中包含的自签名证书生成器

您将收到 x509: certificate signed by unknown authority 警告,因为它是自签名的,因此您需要让您的客户跳过这些警告,方法是将以下内容添加到您的 http.Transport 字段:

 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}

有效的真实证书

最后,如果您要使用真实证书,请将有效证书和密钥保存在可以加载的位置。


这里的关键是使用 server.URL = https://sub.domain.com 提供您自己的域名。

从 Go 1.9+ 开始,您可以使用 func (s *Server) Client() *http.Client in the httptest 包:

Client returns an HTTP client configured for making requests to the server. It is configured to trust the server's TLS test certificate and will close its idle connections on Server.Close.

包中的示例:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "net/http/httptest"
)

func main() {
    ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()

    client := ts.Client()
    res, err := client.Get(ts.URL)
    if err != nil {
        log.Fatal(err)
    }

    greeting, err := io.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", greeting)
}