文章目錄
- 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() //開啟DebugLogclient.R().Get("http://baidu.com/s?wd=req")}
TraceInfo瓶頸分析
trace跟蹤信息
func traceReq() {// Enable trace at request levelclient := 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=12SetQueryParams(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.Bufferclient.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 stringAge 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?動解析結構體或mapfunc getHttpJson() {client := req.C()var respUser uservar respError errorRespres, err := client.R().SetSuccessResult(&respUser). //接收響應回來的json數據,同時也可以解析mapSetErrorResult(&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}}