在Go中嘲笑HTTPS响应 [英] Mocking HTTPS responses in Go

查看:2060
本文介绍了在Go中嘲笑HTTPS响应的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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

  func TestSimple(){
server:= httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,r * http.Request){$ b $设置(Content-Type,application / json)
w.WriteHeader(200)
fmt.Fprintf(w,`{fake:json data这里})
)))
transport:=& http.Transport {
Proxy:func(req * http.Request)(* url.URL,error){
return url.Parse(server.URL)
},
}
//客户端是我的包中发出请求的类型
client:= Client {
c:http.Client {Transport:transport},
}

client.DoRequest()// ...
}

我的包有一个包变量(我希望它是一个常量..),用于查询Web服务的基地址。它是一个https网址。我上面创建的测试服务器是普通的HTTP,没有TLS。



默认情况下,我的测试失败,出现错误tls:first记录看起来不像TLS握手。

为了得到这个结果,我的测试在进行查询之前将包变量更改为普通的http URL而不是https。



有没有办法解决这个问题?我可以使包变量成为一个常量(https),并且可以将 http.Transport 设置为降级为未加密的HTTP,或者使用 httptest .NewTLSServer()改为?



(当我尝试使用 NewTLSServer()时,我从127.0.0.1获得http:TLS握手错误: 45678:tls:oversized record received with length 20037)

解决方案

大部分行为在 net / http 可以被模拟,扩展或改变。虽然 http.Client 是一个实现HTTP客户端语义的具体类型,它的所有字段都被导出并可以定制。



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

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

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



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

  package main 

进口(
fmt
io
记录
净/ http
净/ 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(未能解析httptest.Server URL:,err)
}
http.DefaultClient.Transport = RewriteTransport {URL:u}
resp,err:= http。获取(https://google.com/path-on e)
如果err!= nil {
log.Fatalln(未能发送第一个请求:,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(未能发送第二个请求:,err)
}
fmt.Println([Second Response])
resp.Write(os.Stdout)
}

// RewriteTransport是一个http.RoundTripper,它使用提供的URL的Scheme和Host重写请求
// //,和它的路径作为前缀。
//不透明字段不变。
//如果Transport为零,则使用http.DefaultTransport
type RewriteTransport struct {
Transport http.RoundTripper
URL * url.URL
}

func(t RewriteTransport)RoundTrip(req * http.Request)(* http.Response,error){
//注意url.URL.ResolveReference在这里不起作用
//因为tu是绝对URL
req.URL.Scheme = t.URL.Scheme
req.URL.Host = t.URL.Host
req.URL.Path = path.Join(如果rt == nil {
rt = http.DefaultTransport
}
返回rt.RoundTrip(req)
}

类型HandlerTransport结构{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,
标题:make(http.Header),
正文:r,
Request:req,
}
ready:= make(chan struct {})
prw:=& pipeResponseWriter {r,w,resp,ready}
go func(){
defer w.Close()
thServeHTTP(prw,req)
}()
<-ready
return resp,nil
}

类型pipeResponseWriter结构{$ b $ * 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)

返回wwWrite(p)
}

func(w * pipeResponseWriter)WriteHeader(status int){
if w.ready == nil {
//已经叫做
返回
}
w.resp.StatusCode =状态
w.resp.Status = fmt。 Sprintf(%d%s,status,http.StatusText(status))
close(w.ready)
w.ready = nil
}


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")

解决方案

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\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
}

这篇关于在Go中嘲笑HTTPS响应的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆