窃听Golang http.Client

调试http请求时,我发现自己只想查看原始数据。 我了解httputil.DumpRequest及其合作伙伴httputil.DumpResponse,但是我并不热衷于它们,因为从他们自己的观点出发 ,它们的输出不是真正的东西。

返回的表示形式只是一个近似值; 初始请求的一些详细信息在解析为http.Request时会丢失。 特别是,标题字段名称的顺序和大小写将丢失。

为什么这很重要? 由于信息不正确,我花了很多不必要的时间来调试错误,如果我掌握了正确的信息,我会早点发现问题。
“事后”重新创建并不是实际交换数据的副本。 在大多数情况下, DumpRequest / DumpResponse都足够好,但是我们只能说我已经很努力地学习了这一课。

GET请求

这是我们如何与超时的客户端发出请求。

  t:=&http.Transport { 
ResponseHeaderTimeout:time.Second * 10 //不要永远等待
}

timeoutClient:=&http.Client {
运输:t
}

分别是_:= timeoutClient.Get( “ http://example.com/”
//用resp做点什么

包装器

创建一个使用旧收藏夹io.TeeReaderio.MultiWriternet.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 ,也可以在服务器中使用,但是由于它们通常同时处理许多请求,因此您可能希望将每个连接的输出写入一个单独的文件。 我将在后续帖子中介绍。