golang 使用 elazarl / goproxy 代理 https 请求

平常我们会使用 Fiddler Charles 等抓包工具来抓包分析,但缺点是拓展性比较低,也没法作为一个代理服务器一直在后台代理。
而使用 elazarl / goproxy 这个代理库则可以轻松修改和处理 http(s) 请求,实现自己想要的功能。

本次目的

goproxy 支持两种方式实现 https「中间人攻击」:

  • 类似 fiddler 的代理方式代理流量,简单可用性高,需要在客户端安装信任 CA 证书
  • 透明代理,也称为强制代理(FORCED PROXIES),看名字就知道,是对客户端无感知的强制的,也是经常攻击劫持流量的方式,需要配置在路由器等地方
    由于是在传输层劫持,处理数据实现功能不太方便

我们这次用例的内容是使用客户端代理方式收到解析的 https 明文响应后 do something,下次有空再继续讲讲透明代理。

开始

要用到的第三方库

1
2
3
4
import (
"github.com/elazarl/goproxy"
"github.com/elazarl/goproxy/transport"
)

生成自签名证书

首先我们先来生成用作认证的自签名 CA 证书,使用 openssl 或者其他工具都可以,生成 x.509 PEM格式的密钥对。
go 的标准库中提供一个文件来生成自签名 CA 证书密钥对,执行下面语句:

1
go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost

$GOROOT/src/crypto/tls/ 目录下会出现 cert.pem (公钥/证书)和 key.pem (私钥)文件,
macos 直接双击安装 cert.pem 到钥匙串并完全信任,windows 后缀名改为 .crt 双击安装并分类到系统根证书。

设置代理证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func SetCA(caCert, caKey []byte) error {
goproxyCa, err := tls.X509KeyPair(caCert, caKey)
if err != nil {
return err
}
if goproxyCa.Leaf, err = x509.ParseCertificate(goproxyCa.Certificate[0]); err != nil {
return err
}
goproxy.GoproxyCa = goproxyCa
goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
return nil
}

这段代码中 caCert caKey 两个参数则是刚才生成的证书内容

配置代理服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") // 设置是否输出连接信息
addr := flag.String("addr", ":8080", "proxy listen address") // 监听端口和地址
flag.Parse()
proxy := goproxy.NewProxyHttpServer()
pwd, _ := os.Getwd()
caCert, err := ioutil.ReadFile(CaCertPath) // 设置为你刚才生成的证书路径
if err != nil {
log.Fatal(err)
}
caKey, err := ioutil.ReadFile(CaKeyPath) // 设置为你刚才生成的证书路径
proxy.SetCA(caCert, caKey)
proxy.Verbose = *verbose
proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)

基本配置完毕,接下来就可以监听请求/响应来做一些自己想做的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 监听来自 qq.com 的响应 并在里面做出自己的处理动作
proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp.Request.Host == "qq.com" {
doSomething(resp)
}
return resp
})
l, err := net.Listen("tcp", *addr)
if err != nil {
log.Fatal("listen:", err)
}

log.Println("Starting Proxy")
http.Serve(sl, proxy)

DoFunc()会处理所有传进去的响应,在里面可以自由处理响应再返回给客户端,
由此非常简单地就可以开启一个 https 的代理服务器了,接下来就可以在doSomething()中自由使用 go 来搭建你想要的功能。

Notice: 进入到处理响应这块之后需要对 golang 处理 http 请求有一定了解。
例如假如你要对 resp.Body 响应体全部读取并做处理,那么你读取修改后直接返回你会发现客户端页面拿不到任何请求。
这是因为 golang 中的 http.Responsehttp.RequestBody都是io.ReadCloser类型,当你在读取resp.Body数据后会被自动关闭回收掉。
这在一般情况下都是没问题的,http 请求和响应在读取后就将连接关闭内存清空,但在我们的代理中连接还会被转发使用,所以需要重新再造一个 Body :

1
2
3
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
resp.Body = ioutil.NopCloser(bytes.NewBuffer(buf.Bytes()))

其他用例

修改标头 Header

1
2
3
4
5
proxy.OnRequest().DoFunc(
func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
r.Header.Set("X-GoProxy","yxorPoG-X")
return r,nil
})

返回 nil 作为响应就可以只修改请求继续请求服务器获得响应,如果返回响应的话就会立马将你造的响应返回到客户端而不会请求真实服务器。

工作时间禁止上 reddit

1
2
3
4
5
6
7
8
9
10
proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc(
func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 {
return r,goproxy.NewResponse(r,
goproxy.ContentTypeText,http.StatusForbidden,
"Don't waste your time!")
}
return r,nil
})

更多信息请直接查看源码

GITHUB地址:elazarl/goproxy

golang 使用 go-imap 一键收发中文邮件

使用 go-imap 很轻松就可以完成一个邮件客户端
最近有收取邮件分析的需求,于是就使用 golang 中的 go-imap 包进行邮件处理,将代码和过程分享出来。
首先在 GO MODULE 申明依赖

1
2
3
4
5
6
7
8
9
module Test
go 1.15

require (
github.com/emersion/go-imap v1.2.0
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/emersion/go-message v0.15.0
)

接着新建一个 mail.go 里面来写如何处理邮件
连接邮件服务器 我这里用的是网易邮箱

1
2
3
4
5
6
7
c, err := client.DialTLS("imap.163.com:993", nil)
if err != nil {
log.Fatal(err)
}
log.Println("连接成功")

defer c.Logout()

登录邮件服务器

接着是登录邮件服务器,这里有个地方要注意,基于 RFC 2971 协议 - IMAP4 ID extension 需要
申明自己身份 否则可能会被拒绝登录,收到错误信息,例如网易邮箱会提示 “NO SELECT Unsafe Login”。协议主要约定了客
户端需要定义一个 ID 字段用以表明身份方便统计分析和定位问题,字段
内的字段名不超过 30 个 8 位字节,值不超过 1024 个 8 位字节,
主要字段有以下几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name            Name of the program
version Version number of the program
os Name of the operating system
os-version Version of the operating system
vendor Vendor of the client/server
support-url URL to contact for support
address Postal address of contact/vendor
date Date program was released, specified as a date-time
in IMAP4rev1
command Command used to start the program
arguments Arguments supplied on the command line, if any
if any
environment Description of environment, i.e., UNIX environment
variables or Windows registry settings

而 go-imap 作者 emersion 也有支持 IMAP4 ID extension 的包 go-imap-id 已经导入了 简单使用就行

1
2
3
4
idClient := id.NewClient(c)
idClient.ID(
id.ID{id.FieldName: "IMAPClient", id.FieldVersion: "1.2.0"}, // 随便定义申明自己身份就行
)

选择邮箱文件夹

不多赘述了看代码就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 登录
if err := c.Login("XX@163.com", "pwd"); err != nil {
log.Fatal(err)
}
log.Println("登陆成功")

// 邮箱文件夹列表
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func() {
done <- c.List("", "*", mailboxes)
}()

log.Println("邮箱文件夹:")
for m := range mailboxes {
log.Println("* " + m.Name)
}

if err := <-done; err != nil {
log.Fatal(err)
}

// 选择收件箱
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}

处理邮件正文

处理邮件正文,包里已经封装处理好了,包括多字节字符的处理,只需要调用就行了。这里需要用到 emersion/go-message 包 设置 imap.CharsetReader 以支持除了 UTF-8 和 ASCII 以外的字符编码,如果不设置则支持 UTF-8 和 ASCII ,像 gb2312、gb18030 这些是无法处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
imap.CharsetReader = charset.Reader 
// 获得最新的十封邮件
from := uint32(1)
to := mbox.Messages
if mbox.Messages > 10 {
from = mbox.Messages - 10
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)

messages := make(chan *imap.Message, 10)
section := imap.BodySectionName{}
items := []imap.FetchItem{section.FetchItem()}
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, items, messages)
}()
log.Println("最后十封邮件:")
imap.CharsetReader = charset.Reader
for msg := range messages {
r := msg.GetBody(&section)
if r == nil {
log.Fatal("服务器未返回邮件正文")
}
mr, err := mail.CreateReader(r)
if err != nil {
log.Fatal(err)
}

header := mr.Header
var subject string
if date, err := header.Date(); err == nil {
log.Println("Date:", date)
}
if from, err := header.AddressList("From"); err == nil {
log.Println("From:", from)
}
if to, err := header.AddressList("To"); err == nil {
log.Println("To:", to)
}
if subject, err = header.Subject(); err == nil {
log.Println("Subject:", subject)
}

// 处理邮件正文
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
log.Fatal("NextPart:err ",err)
}

switch h := p.Header.(type) {
case *mail.InlineHeader:
// 正文消息文本
b, _ := ioutil.ReadAll(p.Body)
mailFile := fmt.Sprintf("INBOX/%s.eml",subject)
f, _ := os.OpenFile(mailFile, os.O_RDWR|os.O_CREATE, 0766)
f.Write(b)
f.Close()
case *mail.AttachmentHeader:
// 正文内附件
filename, _ := h.Filename()
log.Printf("attachment: %v\n", filename)
}
}

其中

1
2
section := imap.BodySectionName{}
items := []imap.FetchItem{section.FetchItem()}

这里获取的是全部邮件内容,如果只想获取信封头的话可以使用 imap.FetchEnvelope

相关GITHUB地址:go-imap

我的知乎文章地址:知乎