文章目录
- web网络编程
- Req
- 快速请求
- 调试
- DevMode
- DebugLog
- TraceInfo瓶颈分析
- 控制请求与响应
- 控制请求的字段内容
- 控制调试打印的内容
- 分开dump请求与响应部分
- 请求体设置
- 作用范围级别
- 设置参数查询
- URL 路径参数
- 表单请求设置
- 请求头设置
- 判断响应状态码
- 解析数据
- SetSuccessResult
- gjson
- 响应数据解析练习
- Cookie
- 默认行为
- 禁⽤Cookie
- 存储Cookie
- 证书校验
- 无视风险
- 配置证书
- Auth身份认证
- 文件上传下载
- 上传文件
- 下载文件
- 多线程下载练习
web网络编程
tips:这一章的铺垫比较重要,这章过后就是一些安全工具如何编写以及一些poc、exp的工具编写。
Req
简单的请求
- 客户端创建
- 请求设置
func reqHttp() {
client := req.C() //客户端创建
res, err := client.R(). //请求设置,这个点的意思链式调用,后续在控制请求中会讲
Get("https://httpbin.org/uuid")
if err != nil {
log.Println("请求失败:", err)
}
fmt.Println(res)
}
快速请求
- MustGet
测试使用可以,正式开发不建议使用,可控性差
// 快速使用,一般用在test的时候
func testHttp() {
resp := req.MustGet("https://httpbin.org/uuid") //这里就是发起了一次请求
fmt.Println(resp.String()) //第一种打印*req.Response类型的响应体
fmt.Println(string(resp.Bytes())) //第二种打印*req.Response类型的响应体
}
调试
DevMode
DevMode是直接开启全局调试,自动打印出来
// 调试模式
func devModeReq() {
//使用req进行请求,所以req的调试模式也是在req启动
req.DevMode() //开启调试模式,就会打印出来请求的过程以及响应内容
req.SetCommonBasicAuth("username", "password"). //设置用户名和密码
SetTimeout(5 * time.Second). //设置超时时间
SetUserAgent("my-ua")
resp := req.MustGet("https://httpbin.org/uuid")
//没有开启调试模式但是想打印的话就正常打印
fmt.Println(resp.String()) //第一种打印*req.Response类型的响应体
fmt.Println(string(resp.Bytes())) //第二种打印*req.Response类型的响应体
}
DebugLog
DebugLog 是跟踪请求的过程
他可以看到你整个请求的过程,重定向的信息等等
// 查看请求的过程发生了什么
func deLog() {
client := req.C().
EnableDebugLog() //开启DebugLog
client.R().Get("http://baidu.com/s?wd=req")
}
TraceInfo瓶颈分析
trace跟踪信息
func traceReq() {
// Enable trace at request level
client := req.C()
resp, err := client.R().
EnableTrace(). //开启瓶颈分析
Get("https://api.github.com/users/imroc")
if err != nil {
log.Fatal(err)
}
trace := resp.TraceInfo() //trace跟踪信息
fmt.Println(trace.Blame()) //分析总结(请求减慢的原因归咎)
fmt.Println(trace) // 打印内容
}
控制请求与响应
控制请求的字段内容
这里做一个了解,后面会详细说一下作用范围,这里就过一遍即可,知道哪些字段可控(其实都可控)
func controlReq() {
client := req.C().
SetUserAgent("my-ua"). //设置ua头,在client中设置,在下面的R()中设置不了
//EnableDumpAllToFile("log.txt") //将请求的信息写到该文件中
//捕获请求和响应,
// 想要看的更加详细就可以开启dump所有内容,
// 就能够看到我们是不是真的改变了请求内容
EnableDumpAll()
parms := map[string]string{
"a": "123",
"b": "hello",
}
resp, err := client.R(). //拿到请求体
SetPathParam("usernamae", "imroc"). //设置请求路径,用username作为占位符
SetPathParam("xxx", "test"). //再次设置,用xxx作为占位符
SetQueryParam("a", "12"). //设置请求参数,a=12
SetQueryParams(parms). //用map来作为请求参数,用于多个参数的时候
SetHeader("mycookie", "test"). //设置请求头
SetHeader("mysession", "test2"). //设置请求头
SetBody("body=world"). //设置请求体
Get("https://httpbin.org/uuid")
//以上设置暂时在初期阶段够用了。
if err != nil {
log.Println("请求出错:", err)
}
fmt.Println(resp)
}
控制调试打印的内容
- SetCommonDumpOptions:控制输出的内容
- EnableDumpAllToFile:dump到文件中
输出的log2.txt文件内容如下
// 控制调试打印的内容
// 全局应用
func controlDevOptions() {
client := req.C()
opt := &req.DumpOptions{
Output: os.Stdout, //标准输出
RequestHeader: false, //不输出请求头
RequestBody: false, //不输出请求体
ResponseHeader: true, //输出响应头
ResponseBody: true, //输出响应体
Async: false, //不进行异步输出
}
client.SetCommonDumpOptions(opt).
EnableDumpAllToFile("log2.txt")
client.R().Get("https://httpbin.org/uuid")
}
分开dump请求与响应部分
var bufReq, bufResp bytes.Buffer
:需要用变量来接收请求与响应不同部分- SetDumpOptions:控制dump导出部分
// 分别打印请求与响应部分
// 在R中控制调试打印的内容,只作用与本次R请求,与上面的client直接全局设置的不同
func controlReqRespOutPut() {
client := req.C()
var bufReq, bufResp bytes.Buffer
client.R().
// 不开启的话就会导致无法转储出去单独打印请求体响应体
EnableDump(). //EnableDump启用转储,包括请求和响应的所有内容。
SetDumpOptions(&req.DumpOptions{
RequestOutput: &bufReq, //将请求体输出到这里
ResponseOutput: &bufResp, //将响应体输出到这里
RequestHeader: true,
RequestBody: true,
ResponseHeader: true,
ResponseBody: true,
}).
SetBody("body=hello+world!").
Get("https://httpbin.org/uuid")
fmt.Println("请求体")
fmt.Println(bufReq.String())
fmt.Println("响应体")
fmt.Println(bufResp.String())
}
请求体设置
SetBody 可以接受任意类型
PS: EnableDumpAllWithoutResponse
因为开启了调试模式,所以会打印很多东西,但是可以忽略响应打印出来结果,所以这函数就是忽略响应结果,只看请求。
在编写一些poc/exp的时候可能需要在body部分,需要加入一些结构体或者map数据,他会根据你设置的content-type来判断解析成json还是xml格式,如果都不设置的话他会默认将这些类型解析为json数据body。
添加上content-type类型后就自动转了,比较方便(后面还有更方便的)
- SetBodyXXX
不用设置content-type也能直接转想要的格式了
以下是代码:
// 设置请求体中的一些骚姿势
func setbodyHttp() {
type test struct {
Name string
Age int
}
t := test{
Name: "zhangsan",
Age: 123,
}
client := req.C().DevMode().EnableDumpAllWithoutResponse()
client.R().
SetBody(t).
SetContentType("application/xml").
Get("https://httpbin.org/uuid")
client2 := req.C().DevMode().EnableDumpAllWithoutResponse()
client2.R().
SetBodyXmlMarshal(t).
Get("https://httpbin.org/uuid")
}
作用范围级别
在设置请求参数的时候,有分两种作用范围设置,一种全局(客户端级别),另一种就是只作用与这一个client的对象(请求级别)。
以下就尽量快速过为妙,上面学的时候已经有很多函数都用过了,没必要花太多时间,知道即可,下面作为一个知识字典,以后方便查阅。
请记住:
用req.C().R()来设置的请求级别
用req.C()来设置的是全局级别(也即客户端级别client)
设置参数查询
请求级别:
- SetQueryParam:
SetQueryParam("test", "123")
即 httpxxx?test=123
设置多个的时候仅仅对最后一个设置的生效 - SetQueryParams:SetQueryParams接收map类型设置多个变量,由于map的键唯一,所以多个同名的话也只会作用一个。
- SetQueryString:直接给查询参数即可,
SetQueryString("test1=123&test2=456")
这个不会产生什么重复覆盖问题,字符串给啥他就拼接到你url中去 - AddQueryParam:
AddQueryParam("key", "value1")
如果该参数已存在,它不会覆盖原有的值,这个一般用在你临时需要添加什么查询参数的时候可以用,而且不会覆盖原有的同名键值
全局级别:
- SetCommonQueryParam
- SetCommonQueryParams
- SetCommonQueryString
- AddCommonQueryParam
URL 路径参数
请求级别:
- SetPathParam:
SetPathParam("username", "zhangsan")
- SetPathParams:接收map类型
全局级别:
- SetCommonPathParam
- SetCommonPathParams
表单请求设置
请求级别:
- SetFormData:接收map类型,但是同一个键只能有一个
- SetFormDataFromValues::
代码如下所示,但是注意的是url.Values是net/url包,所以要注意这个url不是我定义的,拿来就用即可。
client3 := req.C().DevMode().EnableDumpAllWithoutResponse()
v := url.Values{
"p": []string{"hello", "world", "!"},
}
client3.R().
SetFormDataFromValues(v).
Post("https://httpbin.org/post")
- multipart ⽅式提交表单:
EnableForceMultipart
client3 := req.C().DevMode().EnableDumpAllWithoutResponse()
v := url.Values{
"p": []string{"hello", "world", "!"},
}
client3.R().
EnableForceMultipart().
SetFormDataFromValues(v).
Post("https://httpbin.org/post")
全局级别:
- SetCommonFormData
- SetCommonFormDataFromValues
请求头设置
请求级别:
- SetHeader:自动将你传进的header键的首字母大写
- SetHeaders:接收map类型,自动将你传进的header键的首字母大写
- SetHeaderNonCanonical:你给什么样就输出什么样,不会自动给你首字母大写
- SetHeadersNonCanonical:你给什么样就输出什么样,不会自动给你首字母大写
- SetHeaderOrder:控制header的顺序,因为有的服务端可能会对header的顺序判断是否允许请求,设置了SetHeaderOrder,他就会按照你给定的顺序进行排序请求过去。
request.SetHeaderOrder(
"cookie",
"ssession",
"test",
"ua",
)
全局级别
- SetCommonHeaderNonCanonical
- SetCommonHeadersNonCanonical
判断响应状态码
没啥好说的,都到这了这些应该对于各位师傅来说都是一眼就学会的基操了。
- resp.IsSuccessState
- resp.IsErrorState
- resp.StatusCode
// 判断响应状态码
func judgeStatusCode() {
client := req.C()
resp, err := client.R().Get("http://www.baidu.com")
if err != nil {
log.Println("请求失败:", err)
}
if resp.IsSuccessState() {
fmt.Println("ok")
}
if resp.IsErrorState() {
fmt.Println("error")
}
//可以通过响应对应的代码判断
if resp.StatusCode != http.StatusOK { //http有一个const变量,里面有很多对应的响应码,自行查看即可(文件:http\status.go)
fmt.Println("error")
}
//打印响应体
fmt.Println(resp.Bytes()) //bytes打印
fmt.Println(resp.String()) //string打印
}
解析数据
SetSuccessResult
-
SetSuccessResult:SetSuccessResult会⾃动解析你给的结构体或map
-
SetErrorResult
这里一同把SetErrorResult也讲了,可以自定义错误解析到你自己定义的错误中去,没啥好说感觉也时,定义好了就直接给到SetErrorResult即可(详细见代码处)
运行结果如下图所示:
// 接收响应回来的json数据
type user struct {
Login string `json:login`
Id int `json:id`
Name string `json:name`
}
// 接收错误响应
type errorResp struct {
Mess string `json:message`
}
// 解析响应的json数据
// SetSuccessResult⾃动解析结构体或map
func getHttpJson() {
client := req.C()
var respUser user
var respError errorResp
res, err := client.R().
SetSuccessResult(&respUser). //接收响应回来的json数据,同时也可以解析map
SetErrorResult(&respError). //若请求错误将错误信息存储到该结构体中
Get("https://api.github.com/users/whoisdhan")
if err != nil {
log.Println("代码请求出错:", err)
return
}
if res.IsSuccessState() {
fmt.Println("请求成功")
fmt.Println(respUser.Login)
fmt.Println(respUser.Id)
fmt.Println(respUser.Name)
} else if res.IsErrorState() {
fmt.Println("网络请求出错:", respError.Mess)
} else {
fmt.Println("未知错误:", res.StatusCode)
}
}
gjson
字段名
、.
:表示读取该字段,字段名.0
表示读取该键名下的数据的第一个字段,字段名.1
读取第二个*
、?
:gjson支持通配符:像在linux命令行中使用的通配符那样用就行,a*a
收尾为a的都匹配#
:表示该字段所有元素。- 在结尾:
字段名1.字段名2.#
表示返回字段名2的数组长度 - 在中间,即
#
后面还有内容:字段名1.#.字段名2
表示返回字段名2所有元素 - 原因:这也是为啥我叫他
#
的意思是表示该字段所有元素,在结尾就是返回长度,不在就是返回整个数组列表
- 在结尾:
gjson解析json数据可提取指定字段,不用定义结构体
gjson非常适合提取几个字段出来之类的,方便的一笔。
// 由于我们的http请求回来的数据能方便的转为string所以也能用gjson
// 模拟数据
const httpjson = `
{
"name":{"first":"Tom", "last": "Anderson"},
"age": 37,
"children": ["Sara", "Alex", "Jack"],
"fav.movie": "Dear Hunter",
"friends": [
{"first": "Dale", "last":"Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
]
}`
func gjsonHttp() {
//先简单用正常的httpjson数据尝试一下
client := req.C()
resp, _ := client.R().
Get("https://api.github.com/users/whoisdhan")
login := gjson.Get(resp.String(), "login")
id := gjson.Get(resp.String(), "id")
name := gjson.Get(resp.String(), "name")
fmt.Println(login, id, name)
//用httpjson测试数据
fmt.Println(gjson.Get(httpjson, "name.first"))
fmt.Println(gjson.Get(httpjson, "friends.#.first"))
fmt.Println(gjson.Get(httpjson, "friends.#.nets"))
fmt.Println(gjson.Get(httpjson, "fav\\.movie")) //字段存在符号的话用\\转义
}
响应数据解析练习
这里的解析可能有点主观了,可以按照自己的想法,尽量不要被我带偏,我的不一定是最佳的
// 练习
// 1. 获取⽤户信息 https://api.github.com/users/{username}
// 2. 获取仓库列表信息 https://api.github.com/users/{username}/repos
// 3. 用户与仓库列表信息:普通读取json、Unmarshal转结构体⽅式解析、gjson读取
// 4. 用户与仓库列表信息:格式化输出到控制台
// 5. 仓库列表信息保存到本地 JSON ⽂件中。
//这里就固定拿仓库列表信息如下信息:
// (也可以随便获取你想要的字段)
// 用户信息{login、id、url、name、email}
// 仓库列表信息{name、owner.login、description}
- myMarshalIndent:为了方便格式化,我自己写了一个格式化函数方便不同方式保存的时候进行格式化
格式化都需要先转到map中存储才能够格式化正常的json数据出来,格式化好了就任意你想干啥就干啥了 - 需要注意的细节:
有的json他是列表包裹着[]
,所以转的时候要注意判断用map[string]interface{}
还是[]map[string]interface{}
- json.MarshalIndent:他可以对map列表进行格式化的,因为那样也时一个正常的json数据,只不过你需要用
[]map[string]interface{}
列表类型进行存储(这里是一个很重要的细节,开发过程中如果不注意很容易导致崩溃)
我用了三个函数完成这个练习:
- normalGetJson:普通读取,就是解析出来后整个data就直接write到文件中
- UnmarshalToStruct:Unmarshal转结构体⽅式解析,这里使用了上面学到的
SetSuccessResult
,给到结构体后进行解析,这样也很方便 - gjsonOutput:这里就是使用gjson了,gjson很方便,但是面对比较多字段提取的时候还是比较繁琐。
这里复习了一个细节:map想要后续不断地赋值的话就需要进行make空间出来才行,如果是[]map的话需要给定一个空间范围,0也行,你要说明这是一个切片。如果你只是单单定义一个map变量或者map切片变量,后续是无法使用的。
还有一个细节就是:make出来的空间是固定的,如果你要make出来的空间作为一个临时变量赋值给其他变量的时候要注意了,用了一个空间就不要继续用了,因为你将一个空间给了多个变量的话,那么那些变量都指向你这一个空间,那么他们的值其实都一样。
运行结果:这里就放几个保存出来的文件截图
-
normalGetJson:
-
UnmarshalToStruct
-
gjsonOutput
示例代码:
// 用户信息{login、id、url、name、email}
// 仓库列表信息{name、owner.login、description}
func gjsonOutput(username string) {
//gjson就十分简单了,只需要拿到响应json数据即可取出来看
mapUserData := make(map[string]interface{}) //用户信息存储
//仓库信息存储,
// 由于仓库是数组列表所以要给一个初始长度
// 因为可能仓库为空的,所以就初始化为0即可
mapReposData := make([]map[string]interface{}, 0)
fmt.Println("-------------------用户信息-------------------")
client := req.C()
resp, err := client.R().
SetPathParam("username", username).
Get("https://api.github.com/users/{username}")
if err != nil {
log.Println("代码请求失败:", err)
}
//gjson想要写入文件就只能转储到struct或者map中
mapUserData["login"] = gjson.Get(resp.String(), "login").String()
mapUserData["id"] = gjson.Get(resp.String(), "id").String()
mapUserData["url"] = gjson.Get(resp.String(), "url").String()
mapUserData["name"] = gjson.Get(resp.String(), "name").String()
mapUserData["email"] = gjson.Get(resp.String(), "email").String()
data, err := json.MarshalIndent(mapUserData, "", "\t")
if err != nil {
log.Println("格式化失败:", err)
}
fmt.Println(string(data))
// test := gjson.Get(resp.String(), "login")
// fmt.Println(test)
fmt.Println("---------------------------------------------")
fmt.Println("-------------------仓库信息-------------------")
client = req.C()
resp, err = client.R().
SetPathParam("username", username).
Get("https://api.github.com/users/{username}/repos")
if err != nil {
log.Println("代码请求失败:", err)
}
// 仓库列表信息{name、owner.login、description}
arr := gjson.Get(resp.String(), "#.name")
for i, j := range arr.Array() {
//一定要放到这里来,因为make指向同一个空间,
// 如果你在for外面定义mapTmpRepos造成取到的值会全部变成最后一个值,
// 因为同一个空间他会同步改变你map切片append里面所有的内容,
// 因为同一个空间嘛
mapTmpRepos := make(map[string]interface{})
mapTmpRepos["name"] = j.String()
mapTmpRepos["login"] = gjson.Get(resp.String(), (strconv.Itoa(i) + ".owner.login")).String()
mapTmpRepos["description"] = gjson.Get(resp.String(), (strconv.Itoa(i) + ".description")).String()
mapReposData = append(mapReposData, mapTmpRepos)
}
data, err = json.MarshalIndent(mapReposData, "", "\t")
if err != nil {
fmt.Println("格式化失败:", err)
}
fmt.Println(string(data)) //格式化的内容打印到终端
file, err := os.OpenFile("gjson.json", os.O_CREATE|os.O_RDWR|os.O_RDWR, 0666)
if err != nil {
log.Println("打开文件失败:", err)
}
_, err = file.Write(data)
if err != nil {
log.Println("导出json失败:", err)
}
}
Cookie
请求级别:
- SetCookies
SetCookies(&http.Cookie{
Name: "hacker",
Value: "aaa",
})
全局级别:
- SetCommonCookies
SetCommonCookies(&http.Cookie{
Name: "Global",
Value: "dddd",
})
默认行为
就是当你请求的服务端响应了set-cookie回来后,当你再次请求的时候就会携带上这个set-cookie回来的cookie键值去请求。
禁⽤Cookie
- SetCookieJar(nil)
存储Cookie
安装
go get -u github.com/juju/persistent-cookiejar
使用
func saveCookies() {
jar, err := cookiejar.New(&cookiejar.Options{
Filename: "cookies.json",
})
if err != nil {
log.Println("保存失败")
}
defer jar.Save()
client := req.C().SetCookieJar(jar).DevMode()
client.R().Get("http://www.baidu.com")
}
证书校验
无视风险
针对一些不安全的网站请求可能会请求失败,所以需要忽略证书的校验
两种方式可以忽略:
client := req.C().DevMode().EnableDumpAllWithoutResponse()
//忽略证书风险
//第一种
//client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
//第二种
client.TLSClientConfig.InsecureSkipVerify = true
_, err := client.R().
Get("https://self-signed.badssl.com/")
if err != nil {
fmt.Println("代码请求失败:", err)
}
配置证书
配置好风险站点的证书即可访问风险站点
证书需要访问网站的时候在浏览器的小锁中下载即可(具体不演示了,自行百度网站证书如何下载)
- 证书文件配置
client := req.C().DevMode().EnableDumpAllWithoutResponse()
//可以同时配置多个网站的证书
client.SetRootCertsFromFile("cert1.crt","cert2.crt","cert3.crt")
- 证书内容配置
在下载了证书之后,可以编辑证书将里面的内容配置进来也行
client := req.C().DevMode().EnableDumpAllWithoutResponse()
client.SetRootCertFromString("-----BEGIN CERTIFICATE-----")
Auth身份认证
- SetBasicAuth
- SetDigestAuth
区别:
一个Basic认证,一个Digest认证
Digest认证比较安全,请求被拦截了攻击者无法直接获取密码
但是Basic的请求被拦截了就是直接获取到密码
服务端支持哪一个?
检查WWW-Authenticate
响应头,返回Basic 还是 Digest就知道支持哪一个。
func authHttp() {
client := req.C().DevMode().EnableDumpAllWithoutResponse()
client.R().
SetBasicAuth("username", "password").
Get("https://httpbin.org/uuid")
client2 := req.C().DevMode().EnableDumpAllWithoutResponse()
client2.R().
SetDigestAuth("username2", "password2").
Get("https://httpbin.org/uuid")
}
文件上传下载
上传文件
- SetFile
- SetFiles:接收map类型,上传多个文件
- SetUploadCallback:显示上传进度
func uploadFileHttp() {
//简单上传
client := req.C().DevMode().EnableDumpAllWithoutResponse()
callback := func(info req.UploadInfo) { //显示上传进度
fmt.Printf("\n文件名:%q\n已上传:%.2f%%\n",
info.FileName,
float64(info.UploadedSize)/float64(info.FileSize)*100.0)
}
client.R().
//SetFile("filename", "cookies.json").
SetFiles(map[string]string{
"test": "test.txt",
"test2": "test2.txt",
"test3": "test3.txt",
}).
SetUploadCallback(callback). //使用该函数:显示上传进度
Post("https://httpbin.org/post")
}
下载文件
- SetOutputFile:这里是指定下载的文件路径
- SetOutputDirectory:设置下载的默认路径,即SetOutputFile可以只给文件名,自动存在该路径下
- SetDownloadCallback:显示下载进度
func downloadFileHttp() {
client := req.C() //.DevMode().EnableDumpAllWithoutResponse()
callback := func(info req.DownloadInfo) {
if info.Response.Response != nil { //响应不为空
fmt.Printf("\n已下载:%.2f%%\n",
float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
}
}
client.R().
//这里是指定文件路径
SetOutputFile("./baidu.html").Get("http://127.0.0.1/xxx.txt")
client2 := req.C().
//这里可以设置默认下载目录,后面直接download的时候就不用指定路径了,给文件名即可
SetOutputDirectory("./").
DevMode().
EnableDumpAllWithoutResponse()
client2.R().
SetOutputFile("baidu2.html").
SetDownloadCallback(callback).
Get("http://127.0.0.1/xxx.txt")
}
多线程下载练习
- NewParallelDownload:创建一个多线程下载客户端
- 其他函数在注释中标明了。
func threatDownload() {
client := req.C()
err := client.NewParallelDownload("http://xxxxx.xxxx.xxx/xxx.iso").
SetConcurrency(5). //设置 5 个线程 并行下载
SetFileMode(0777). //设置 文件权限(可读可写可执行)。
SetOutputFile("xxx.iso"). //设定最终 存储文件名
SetSegmentSize(1024 * 1024 * 5). //每个线程下载 5MB 的数据块
SetTempRootDir("./tmp"). //这个是下载的时候指定的临时存储目录
Do()
if err != nil {
log.Println("下载失败:", err)
return
}
}