dt3674 2015-01-10 20:32
浏览 85
已采纳

在Go中模拟HTTPS响应

I'm trying to write tests for a package that makes requests to a web service. I'm running into issues probably due to my lack of understanding of TLS.

Currently my test looks something like this:

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() // ...
}

My package has a package variable (I'd like for it to be a constant..) for the base address of the web service to query. It is an https URL. The test server I created above is plain HTTP, no TLS.

By default, my test fails with the error "tls: first record does not look like a TLS handshake."

To get this to work, my tests change the package variable to a plain http URL instead of https before making the query.

Is there any way around this? Can I make the package variable a constant (https), and either set up a http.Transport that "downgrades" to unencrypted HTTP, or use httptest.NewTLSServer() instead?

(When I try to use NewTLSServer() I get "http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037")

  • 写回答

2条回答 默认 最新

  • dongtuojuan8998 2015-01-12 03:24
    关注

    Most of the behavior in net/http can be mocked, extended, or altered. Although http.Client is a concrete type that implements HTTP client semantics, all of its fields are exported and may be customized.

    The Client.Transport field, in particular, may be replaced to make the Client do anything from using custom protocols (such as ftp:// or file://) to connecting directly to local handlers (without generating HTTP protocol bytes or sending anything over the network).

    The client functions, such as http.Get, all utilize the exported http.DefaultClient package variable (which you may modify), so code that utilizes these convenience functions does not, for example, have to be changed to call methods on a custom Client variable. Note that while it would be unreasonable to modify global behavior in a publicly-available library, it's very useful to do so in applications and tests (including library tests).

    http://play.golang.org/p/afljO086iB contains a custom http.RoundTripper that rewrites the request URL so that it'll be routed to a locally hosted httptest.Server, and another example that directly passes the request to an http.Handler, along with a custom http.ResponseWriter implementation, in order to create an http.Response. The second approach isn't as diligent as the first (it doesn't fill out as many fields in the Response value) but is more efficient, and should be compatible enough to work with most handlers and client callers.

    The above-linked code is included below as well:

    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
    ", 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("
    ", strings.Repeat("-", 80), "
    
    ")
    
        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
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(1条)

报告相同问题?

悬赏问题

  • ¥15 前端echarts坐标轴问题
  • ¥15 CMFCPropertyPage
  • ¥15 ad5933的I2C
  • ¥15 请问RTX4060的笔记本电脑可以训练yolov5模型吗?
  • ¥15 数学建模求思路及代码
  • ¥50 silvaco GaN HEMT有栅极场板的击穿电压仿真问题
  • ¥15 谁会P4语言啊,我想请教一下
  • ¥15 这个怎么改成直流激励源给加热电阻提供5a电流呀
  • ¥50 求解vmware的网络模式问题 别拿AI回答
  • ¥24 EFS加密后,在同一台电脑解密出错,证书界面找不到对应指纹的证书,未备份证书,求在原电脑解密的方法,可行即采纳