调试http请求时,我发现自己只想查看原始数据。 我了解httputil.DumpRequest及其合作伙伴httputil.DumpResponse,但是我并不热衷于它们,因为从他们自己的观点出发 ,它们的输出不是真正的东西。
返回的表示形式只是一个近似值; 初始请求的一些详细信息在解析为http.Request时会丢失。 特别是,标题字段名称的顺序和大小写将丢失。
为什么这很重要? 由于信息不正确,我花了很多不必要的时间来调试错误,如果我掌握了正确的信息,我会早点发现问题。
“事后”重新创建并不是实际交换数据的副本。 在大多数情况下, DumpRequest / DumpResponse都足够好,但是我们只能说我已经很努力地学习了这一课。
GET请求
这是我们如何与超时的客户端发出请求。
- 小镇上的新孩子-Go的同步地图
- KrakenD API Gateway 0.8发布
- จะใช้CookieทำAPIแทนJWTต้องทำอะไรบ้าง?
- Go模块:版本控制和依赖性管理
- Golang的Oauth黑魔法
t:=&http.Transport {
ResponseHeaderTimeout:time.Second * 10 //不要永远等待
}
timeoutClient:=&http.Client {
运输:t
}
分别是_:= timeoutClient.Get( “ http://example.com/” )
//用resp做点什么
包装器
创建一个使用旧收藏夹io.TeeReader和io.MultiWriter的net.Conn包装器,以输出对stderr的读写。
// spyConnection包装了一个net.Conn,所有的读写都通过WrapConnection()输出到stderr。
输入 spyConnection struct {
康恩
io.Reader
作家
}
//读取将从基础连接读取的所有数据写入sc.Writer。
func (sc * spyConnection)读取(b [] byte)(整数,错误){
返回 sc.Reader.Read(b)
}
// Write将所有写入基础连接的数据写入sc.Writer。
func (sc * spyConnection)Write(b [] byte)(int,error){
返回 sc.Writer.Write(b)
}
// WrapConnection包装一个现有的连接,所有读/写的数据都写入w(如果w == nil,则为os.Stderr)。
func WrapConnection(c net.Conn,输出io.Writer)net.Conn {
如果输出== nil {
输出= os.Stderr
}
返回 &spyConnection {
康恩:
阅读器:io.TeeReader(c,output),
编写者:io.MultiWriter(output,c),
}
}
现在我们只需要获取连接即可进行包装。
拨号
http.Transport采用Dial函数,该函数返回已建立的连接(在本例中为包装的连接)。
拨号程序:=&net.Dialer {
超时:30 *时间。 第二 ,
KeepAlive:30 *时间。 第二 ,
}
Dial:= func (网络,地址字符串)(net.Conn,错误){
conn,err:= dialer.Dial(网络,地址)
如果 err!= nil {
返回 nil,err
}
返回 WrapConnection(conn,os.Stderr},nil //返回一个包装好的网络。
}
t:=&http.Transport {
ResponseHeaderTimeout:10 *时间。 第二 ,
DisableCompression: true ,//人类无法读取压缩的响应
拨号:拨号, //在现有交通工具中使用新的拨号
}
因此,尽管不幸的是,我们不能窃听HTTP连接,但是可以利用HTTP连接进行窃听。
TLS
方便的http.Transport还使您可以提供与Dial函数几乎相同的DialTLS函数。 这是我的版本,或多或少地复制了当您不提供DialTLS功能时发生的情况。
DialTLS:= func (网络,地址字符串)(net.Conn,错误){
plainConn,err:= dialer.Dial(网络,地址)
如果 err!= nil {
返回 nil,err
}
//启动TLS并根据证书检查远程主机名。
cfg:= new(tls.Config)
//添加https://以满足url.Parse(),我们将不再使用它
u,err:= url.Parse(fmt.Sprintf( “ https://%s” ,地址))
如果 err!= nil {
返回 nil,err
}
serverName:= u.Host [:strings.LastIndex(u.Host, “:” )]
cfg.ServerName =服务器名称
tlsConn:= tls.Client(plainConn,cfg)
errc:= make( chan错误,2)
计时器:= time.AfterFunc(time。Second, func (){
errc <-errors.New( “ TLS握手超时” )
})
go func (){
错误:= tlsConn.Handshake()
timer.Stop()
errc <-错误
}()
如果 err:= <-errc; err!= nil {
plainConn.Close()
返回 nil,err
}
如果 !cfg.InsecureSkipVerify {
如果 err:= tlsConn.VerifyHostname(cfg.ServerName); err!= nil {
plainConn.Close()
返回 nil,err
}
}
返回 WrapConnection(tlsConn,os.Stderr,nil //包装结果conn
}
现在我们开始营业。

工作代码示例
这是我们的完整代码(以及由于游乐场的限制而无法使用的游乐场链接)。
包主
导入 (
“ crypto / tls”
“错误”
“ fmt”
“ io”
“ io / ioutil”
“净”
“ net / http”
“网络/网址”
“ os”
“弦乐”
“时间”
)
// WrapConnection包装一个现有的连接,所有读/写的数据都写入w(如果w == nil,则为os.Stderr)。
func WrapConnection(c net.Conn,输出io.Writer)net.Conn {
返回 &spyConnection {
康恩:
阅读器:io.TeeReader(c,output),
编写者:io.MultiWriter(output,c),
}
}
// spyConnection包装了一个net.Conn,所有的读写都通过WrapConnection()输出到stderr。
输入 spyConnection struct {
康恩
io.Reader
作家
}
//读取将从基础连接读取的所有数据写入sc.Writer。
func (sc * spyConnection)读取(b [] byte)(整数,错误){
返回 sc.Reader.Read(b)
}
// Write将所有写入基础连接的数据写入sc.Writer。
func (sc * spyConnection)Write(b [] byte)(int,error){
返回 sc.Writer.Write(b)
}
func main(){
拨号程序:=&net.Dialer {
超时:30 *时间。 第二 ,
KeepAlive:30 *时间。 第二 ,
}
Dial:= func (网络,地址字符串)(net.Conn,错误){
conn,err:= dialer.Dial(网络,地址)
如果 err!= nil {
返回 nil,err
}
fmt.Fprint(os.Stderr,fmt.Sprintf( “ \ n%s \ n \ n”,strings.Repeat(“-”,80) ))
返回 WrapConnection(conn,os.Stderr),nil //返回包装好的网络。
}
DialTLS:= func (网络,地址字符串)(net.Conn,错误){
plainConn,err:= dialer.Dial(网络,地址)
如果 err!= nil {
返回 nil,err
}
//启动TLS并根据证书检查远程主机名。
cfg:= new(tls.Config)
//添加https://以满足url.Parse(),我们将不再使用它
u,err:= url.Parse(fmt.Sprintf( “ https://%s” ,地址))
如果 err!= nil {
返回 nil,err
}
serverName:= u.Host [:strings.LastIndex(u.Host, “:” )]
cfg.ServerName =服务器名称
tlsConn:= tls.Client(plainConn,cfg)
errc:= make( chan错误,2)
计时器:= time.AfterFunc(time。Second, func (){
errc <-errors.New( “ TLS握手超时” )
})
go func (){
错误:= tlsConn.Handshake()
timer.Stop()
errc <-错误
}()
如果 err:= <-errc; err!= nil {
plainConn.Close()
返回 nil,err
}
如果 !cfg.InsecureSkipVerify {
如果 err:= tlsConn.VerifyHostname(cfg.ServerName); err!= nil {
plainConn.Close()
返回 nil,err
}
}
fmt.Fprint(os.Stderr,fmt.Sprintf( “ \ n%s \ n \ n”,strings.Repeat(“-”,80) ))
返回 WrapConnection(tlsConn,os.Stderr),nil //返回包装好的网络。
}
t:=&http.Transport {
拨号:拨号,
DialTLS:DialTLS,
DisableCompression: true , //人类无法读取压缩的响应
TLSHandshakeTimeout:10 *时间。 第二 ,
ResponseHeaderTimeout:10 *时间。 第二 ,
ExpectContinueTimeout:1 *时间。 第二 ,
}
timeoutClient:=&http.Client {
运输:t
}
// http
分别是_:= timeoutClient.Get( “ http://example.com/” )
//除非我们强制读取正文,否则读取是不完整的
ioutil.ReadAll(resp.Body)
resp.Body.Close()
// https
resp,_ = timeoutClient.Get( “ https://example.com/” )
ioutil.ReadAll(resp.Body)
resp.Body.Close()
time.Sleep(时间。 秒 )
}
包
自从我开始编写此代码以来,我决定将其转换为一个包,请参见https://github.com/j0hnsmith/connspy。
超越http
POP3,IMAP,DNS,redis,请给它命名……任何TCP / IP连接都可以用SpyConnection包装(有用的是,协议应为纯文本)。 所有连接都必须在该行的某个位置调用Dialer.Dial ,而SpyConnection可用于包装生成的net.Conn 。
服务器也
同样的技术也可以适用于net.Listener ,也可以在服务器中使用,但是由于它们通常同时处理许多请求,因此您可能希望将每个连接的输出写入一个单独的文件。 我将在后续帖子中介绍。