2023-2024-1 浅尝黑魔法HTTP框架 Req
本文旨在总结与分享笔者的学习经历,能力有限若有不确之处,望请交流指正.
技术简介
在自动化开发中,经常面临HTTP
请求模拟,经常用有如Postman
,Burp
这样带网络调试能力的工具,当我编写其他语言时有XMLHttpRequest
、Reqwest
、Axios
这类HTTP框架,但如果需要用go来解决这一切呢?今天我要向你介绍。
import "github.com/imroc/req"
Req 是一个简单易用还带黑魔法的 Go HTTP 客户端,写更少的代码,获得更高的效率.
初探请求
一个完备的HTTP
客户端请求库具有起码的GET/POST
的功能,用req
发送请求的几种方式:
GET
发送一次简单的GET
func main() {
//req.C() => client创建客户端
client := req.C() //.
//req.C().R() => request请求回应
resp, err := client.R().Get("http://whois.pconline.com.cn/ipJson.jsp")
if err != nil {
log.Fatal(err)
}
//IPCallBack({"ip":"58.249....","pro":"广东省
fmt.Println(resp)
}
自动解码&序列化
req
默认会嗅探消息,对接收到的消息主体自动解码,req也提供了消息序列的能力。
source: req.cool/zh/docs/tutorial/auto-decode/
- 首先检测
Content-Type
头,如果是文本内容类型(json、xml、html 等),req
会尝试解码,如果不是,则不会解码。- 如果没有
Content-Type
头,会提取响应体首部内容进行嗅探,如果嗅探出来明确不是 UTF-8,则自动解码为 UTF-8,如果字符集不确定,则不会尝试解码。source: req.cool/zh/docs/tutorial/marshal-unmarshal/
Req
默认使用标准库的 Marshal 和 Unmarshal 实现:
- 如果将 map 或 struct 传入
Request.SetBody
,Request.SetBodyJsonMarshal
orRequest.SetBodyXmlMarshal
,会使用json.Marshal
或xml.Marshal
来将请求体 Marshal 成指定编码格式的内容。- 如果将 map 或 struct 传入
Request.SetResult
,Request.SetError
或Response.Unmarshal
,会根据编码格式使用json.Unmarshal
或xml.Unmarshal
来响应体 Unmarshal 到传入的 map 或 struct 中。
DO-GET
req支持DO
风格的请求方式,链式调用顺序的写法使得请求代码的可读性更好。
下面是一段展示req自动解码&序列能力的DO风格请求代码:
type info struct {
Ip string `json:"ip"`
Pro string `json:"pro"`
City string `json:"city"`
Addr string `json:"addr"`
Err string `json:"err"`
}
func main() {
client := req.C()
var resp info
err := client.
Get("http://whois.pconline.com.cn/ipJson.jsp").
AddQueryParams("ip", "114.114.114.114").
AddQueryParams("json", "true").
Do().
Into(&resp)
if err != nil {
log.Fatal(err)
}
if resp.Err != "" {
log.Fatal(resp.Err)
}
//114.114.114.114 江苏省南京市 电信
fmt.Println(resp.Ip, resp.Addr)
}
DO-POST
同样的,POST请求也很简单,以百度翻译接口为例
type Word struct {
Data []struct {
K string `json:"k"`
V string `json:"v"`
} `json:"data"`
Logid int64 `json:"logid"`
}
func main() {
client := req.C()
var resp Word
err := client.
Post("https://fanyi.baidu.com/sug").
SetBody("kw=apple").
SetHeader("content-type", "application/x-www-form-urlencoded").
Do().
Into(&resp)
if err != nil {
log.Fatal(err)
}
//[{Apple n. 苹果公司,原称苹果电脑公司} {apple n. 苹果树} ...
fmt.Println(resp.Data)
}
记录与携带本地Cookie
在业务查询时,我们经常需要携带token来保持自己访问权限,典型的有如Jsessionid、ASPsession。Req会自动保存和添加Cookie,简化了这些非常麻烦的请求头设置操作,同时还支持将Cookie持久化存储到本地,让程序启动时调用,结束时自动保存。
source : req.cool/zh/docs/tutorial/cookie/ req 默认启用了 cookie 存储,当 server 返回的 header 中包含
Set-Cookie
时,req 默认会自动存储并在后续需要 cookie 的请求中自动带上 cookie 请求头。
这是一段简单的携带Cookie查询用户哔哩哔哩
经验记录的的代码:
client := req.C()
client.
SetTimeout(10 * time.Second)
type log struct {
Code int `json:"code"`
Message string `json:"message"`
TTL int `json:"ttl"`
Data struct {
List []struct {
Delta int `json:"delta"`
Time string `json:"time"`
Reason string `json:"reason"`
} `json:"list"`
Count string `json:"count"`
} `json:"data"`
}
var loga log
client.
Get("https://api.bilibili.com/x/member/web/exp/log").
SetHeader("cookie", "SESSDATA=....").
Do().
Into(&loga)
if loga.Code == 0 {
for _, v := range loga.Data.List {
fmt.Println("经验:" + strconv.Itoa(v.Delta) + " 时间:" + v.Time + " 事件:" + v.Reason)
}
}
/*
经验:5 时间:2023-10-06 00:00:12 事件:观看视频奖励
经验:5 时间:2023-10-06 00:00:12 事件:登录奖励
经验:5 时间:2023-10-05 18:59:12 事件:观看视频奖励
....
*/
通常在go
网络编程里,我们会常用类似CookieJar
来存储与管理Cookie
,req框架也支持自定义的Cookie
存储能力,以Cookiejar
为例,读取与保存到本地json。
import (
"fmt"
"log"
"strconv"
"time"
"github.com/imroc/req/v3"
cookiejar "github.com/juju/persistent-cookiejar"
)
func client() (*req.Client, *cookiejar.Jar) {
jar, err := cookiejar.New(&cookiejar.Options{
Filename: "cookies.json",
})
if err != nil {
log.Fatalf("failed to create persistent cookiejar: %s\n", err.Error())
}
client := req.C()
client.
SetTimeout(10 * time.Second).
SetCookieJar(jar) //自定义存储
return client, jar
}
func main() {
type log struct {
Code int `json:"code"`
Message string `json:"message"`
TTL int `json:"ttl"`
Data struct {
List []struct {
Delta int `json:"delta"`
Time string `json:"time"`
Reason string `json:"reason"`
} `json:"list"`
Count string `json:"count"`
} `json:"data"`
}
var loga log
client, jar := client()
defer jar.Save() //保存
client.
Get("https://api.bilibili.com/x/member/web/exp/log").
Do().
Into(&loga)
if loga.Code == 0 {
for _, v := range loga.Data.List {
fmt.Println("经验:" + strconv.Itoa(v.Delta) + " 时间:" + v.Time + " 事件:" + v.Reason)
}
} else {
fmt.Println(loga.Message) //账号未登录
}
}
代理请求
有时候受环境限制,需要通过代理网络才能完成请求,req
也支持了客户端的代理设置,以请求ip地址为例。
func client() *req.Client {
client := req.C()
client.
SetProxyURL("socks5://localhost:7890")
return client
}
func main() {
type info struct {
Ip string `json:"ip"`
Pro string `json:"pro"`
City string `json:"city"`
Addr string `json:"addr"`
Err string `json:"err"`
}
var resp info
client().
Get("http://whois.pconline.com.cn/ipJson.jsp").
AddQueryParams("json", "true").
Do().
Into(&resp)
/*
PS ...> go run A.go
广东省广州市 广东工贸职业技术学院
PS ...> go run B.go
新加坡....数据中心
*/
fmt.Println(resp.Addr)
}
HTTP指纹模拟
我们知道,传统的HTTP
请求特征比较多,而现在依赖来源IP,来源UA
的检测信任正在降低,与之取代的是HTTP指纹
。HTTP指纹
是利用了如TLS
握手特征,HTTP2
属性帧的值和排列顺序,HTTP
特殊请求头等等进行验证,当服务端在检测某些特征来源在高频次的请求后予以处理。
在传统的Request
等请求库中缺失了针对这些特征检查的模拟能力,而req
最新版本声称支持了对于HTTP
的指纹特征的自定义伪装。
source : req.cool/zh/blog/已支持-http-指纹伪装轻松绕过反爬虫检测
……
HTTP 指纹是由一系列特征构成的,服务端检查的特征越详细(反爬级别越高),伪装的难度就越高,当然如果我们把所有特征全都伪装了,那就一定就能骗过服务端了。
常见的一些特征:
- User-Agent 的值。
- Header 及其排列顺序。
- TLS 指纹,也就是TLS 握手时,客户端发送 ClientHello 的特征。包含客户端所支持的加密套件列表及其排列顺序、客户端支持的TLS版本、压缩方式、TLS extensions 列表及其排列顺序等。
对于 HTTP2,还有更多额外可以检查的特征:
- HTTP2 settings 帧中的值列表及其排列顺序。
- HTTP2
WINDOW_UPDATE
帧的值。- HTTP2 Priority 帧列表及其排列顺序。
- 伪头(Pseudo-Header) 的顺序。
- 请求头及其排列顺序,以及 header 帧中 flag 与 priority 选项的值。
以上所有特征,在
req
中都可以轻松模拟,实现伪装成任何想要的浏览器客户端。
以ja3
检测为例,传统的客户端请求,会生成固定的ja3
指纹:
func client() *req.Client {
client := req.C()
return client
}
func main() {
type info struct {
Hash string `json:"ja3_hash"`
Text string `json:"ja3_text"`
}
var resp info
client().
Get("https://tls.browserleaks.com/json").
Do().
Into(&resp)
/*
PS ...> go run C.go
7a15285d4efc355608b304698cd7f9ab 771,49195-49199-49196-49200-52393....
PS ...> go run C.go
7a15285d4efc355608b304698cd7f9ab 771,49195-49199-49196-49200-52393....
*/
fmt.Println(resp.Hash, resp.Text)
}
设置客户端伪装后:
package main
import (
"fmt"
"github.com/imroc/req/v3"
)
func client() *req.Client {
client := req.C()
client.
ImpersonateChrome() //伪装Chrome
return client
}
func main() {
type info struct {
Hash string `json:"ja3_hash"`
Text string `json:"ja3_text"`
}
var resp info
client().
Get("https://tls.browserleaks.com/json").
Do().
Into(&resp)
/*
PS ...> go run C.go
c310f0fbecd660195ed31f273f938e05 771,4865-4866-4867-49195-49199-49196-49200-52393
PS ...> go run C.go
0d6c3d08a9204af8a40f24b9168cd090 771,4865-4866-4867-49195-49199-49196-49200-52393
*/
fmt.Println(resp.Hash, resp.Text)
}