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

对于目前的网络环境有感

目前的网络环境愈发混乱,噪音太多,想着还是重启个人博客吧,留个自己随便怎么写的地方,自由一点。

打开各类社交媒体,真的是群魔乱舞,阴阳怪气,性别对立,疫情导致的对立,俄乌战争的双方站队让我真的是疲惫。
即使点了不感兴趣,「智能」的推荐算法还是会基于热度来推荐给我,觉得我大概率会喜欢,或者说感兴趣,
我似乎已经在一个编织好的信息茧房里了,咀食着系统投喂给我的「适合」我的食粮,而这些热度高的东西仅仅是因为制造对立调动情绪让大家都无可避免的吸引进去。
真的因为这些内容质量高吗?不过是利用人性的弱点让人无法避免地去加入一场场的争论,看一个个谎言,增加自我的消耗罢了。

是时候跳脱出来了,于是我重新开始看论坛式的 v2ex,只看技术关注内容,把情绪抽离出来。发觉自己做事的专注力又回来了,
重启了个人博客,保持一定的独立性并且分享自己的经验,虽然力量不大,学识不多,但应该也能帮到需要这些东西的人。
我觉得互联网不该是这个样子的,发展了几十年了,自由开放的互联网反而在某种程度上却非常封闭,各大公司把持数据绑架用户,
把用户和内容产出都封在了自己的世界里,说不上对错,只是我觉得互联网世界似乎不应该是这样的,回想 pc 的 web2.0 时代甚至产生已经不是这个同一个世界的错觉。

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

我的知乎文章地址:知乎