├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── body ├── body.go ├── form.go ├── raw.go └── url.go ├── client.go ├── cookiejar.go ├── go.mod └── request.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | httpc 2 | ======= 3 | [![GoDoc](https://godoc.org/github.com/Albert-Zhan/goreq?status.svg)](https://godoc.org/github.com/Albert-Zhan/httpc) 4 | [![License](https://img.shields.io/badge/license-apache2-blue.svg)](LICENSE) 5 | 6 | **Go的一个功能强大、易扩展、易使用的http客户端请求库。适合用于接口请求,模拟浏览器请求,爬虫请求。** 7 | 8 | ## 特点 9 | 10 | - Cookie管理器(适合爬虫和模拟请求) 11 | - 支持HEADER、GET、POST、PUT、DELETE 12 | - 轻松上传文件下载文件 13 | - 支持链式调用 14 | 15 | ## 安装 16 | 17 | ```shell 18 | go get github.com/Albert-Zhan/httpc 19 | ``` 20 | 21 | ## API文档 22 | 23 | [httpc在线文档](https://godoc.org/github.com/Albert-Zhan/httpc) 24 | 25 | ## 快速入门 26 | 27 | ### 1. 简单的请求 28 | 29 | ```go 30 | //新建一个请求和http客户端 31 | req:=httpc.NewRequest(httpc.NewHttpClient()) 32 | //get请求,返回string类型的body 33 | resp,body,err:=req.SetUrl("http://127.0.0.1").Send().End() 34 | if err!=nil { 35 | fmt.Println(err) 36 | }else{ 37 | fmt.Println(resp) 38 | fmt.Println(body) 39 | } 40 | ``` 41 | 42 | ### 2. 设置头信息 43 | 44 | ```go 45 | //新建一个http客户端 46 | client:=httpc.NewHttpClient() 47 | //新建一个请求 48 | req:=httpc.NewRequest(client) 49 | req.SetMethod("post").SetUrl("http://127.0.0.1") 50 | //设置头信息,返回byte类型的body 51 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 52 | if err!=nil { 53 | fmt.Println(err) 54 | }else{ 55 | fmt.Println(resp) 56 | fmt.Println(bodyByte) 57 | } 58 | ``` 59 | 60 | ### 3. 设置请求信息(get) 61 | 62 | ```go 63 | //新建一个http客户端 64 | client:=httpc.NewHttpClient() 65 | //新建一个请求 66 | req:=httpc.NewRequest(client) 67 | req.SetMethod("post").SetUrl("http://127.0.0.1") 68 | //设置头信息 69 | req.SetHeader("HOST","127.0.0.1") 70 | //设置请求信息 71 | resp,body,err:=req.SetParam("client", "httpc").Send().End() 72 | if err!=nil { 73 | fmt.Println(err) 74 | }else{ 75 | fmt.Println(resp) 76 | fmt.Println(body) 77 | } 78 | ``` 79 | 80 | ### 4. 设置请求信息(post) 81 | 82 | ```go 83 | //新建一个http客户端 84 | client:=httpc.NewHttpClient() 85 | //新建一个请求 86 | req:=httpc.NewRequest(client) 87 | req.SetMethod("post").SetUrl("http://127.0.0.1") 88 | //设置头信息 89 | req.SetHeader("HOST","127.0.0.1") 90 | //设置请求信息 91 | b:=body.NewUrlEncode() 92 | b.SetData("client","httpc") 93 | resp,body,err:=req.SetBody(b).Send().End() 94 | if err!=nil { 95 | fmt.Println(err) 96 | }else{ 97 | fmt.Println(resp) 98 | fmt.Println(body) 99 | } 100 | ``` 101 | 102 | ### 5. 设置Cookie 103 | 104 | ```go 105 | //新建一个http客户端 106 | client:=httpc.NewHttpClient() 107 | //新建一个请求 108 | req:=httpc.NewRequest(client) 109 | //设置请求地址和头信息 110 | req.SetUrl("http://127.0.0.1").SetHeader("HOST","127.0.0.1") 111 | //设置请求数据 112 | req.SetData("client", "httpc") 113 | var cookies []*http.Cookie 114 | cookie:=&http.Cookie{Name:"client",Value:"httpc"} 115 | cookies= append(cookies, cookie) 116 | //添加cookie并请求 117 | resp,bodyByte,err:=req.SetCookies(&cookies).Send().End() 118 | if err!=nil { 119 | fmt.Println(err) 120 | }else{ 121 | fmt.Println(resp) 122 | fmt.Println(bodyByte) 123 | } 124 | ``` 125 | 126 | ### 6. 上传文件 127 | 128 | ```go 129 | //新建一个http客户端 130 | client:=httpc.NewHttpClient() 131 | //新建一个请求 132 | req:=httpc.NewRequest(client) 133 | req.SetMethod("post").SetUrl("http://127.0.0.1") 134 | //设置上传的文件 135 | b:=body.NewFormData() 136 | b.SetFile("img1","./img.png") 137 | //设置附加参数 138 | b.SetData("client","httpc") 139 | resp,body,err:=req.SetBody(b).Send().End() 140 | if err!=nil { 141 | fmt.Println(err) 142 | }else{ 143 | fmt.Println(resp) 144 | fmt.Println(body) 145 | } 146 | ``` 147 | 148 | ### 7. 下载文件 149 | 150 | ```go 151 | //新建一个http客户端 152 | client:=httpc.NewHttpClient() 153 | //新建一个请求 154 | req:=httpc.NewRequest(client) 155 | //请求保存文件 156 | resp,body,err:=req.SetUrl("http://127.0.0.1/1.zip").Send().EndFile("./test/","") 157 | if err!=nil { 158 | fmt.Println(err) 159 | }else{ 160 | fmt.Println(resp) 161 | fmt.Println(body) 162 | } 163 | ``` 164 | 165 | ### 8. 开启调试 166 | 167 | ```go 168 | req:=httpc.NewRequest(httpc.NewHttpClient()) 169 | req.SetMethod("post").SetUrl("https://127.0.0.1") 170 | req.SetHeader("HOST","127.0.0.1") 171 | b:=body.NewUrlEncode() 172 | b.SetData("client","httpc") 173 | var cookies []*http.Cookie 174 | cookie:=&http.Cookie{Name:"client",Value:"httpc"} 175 | cookies= append(cookies, cookie) 176 | _, _, _ = req.SetBody(b).SetCookies(&cookies).SetDebug(true).Send().End() 177 | ``` 178 | 179 | > ⚠ 在实际场景中不建议复用Request,建议每个请求对应一个Request。 180 | 181 | ## 高级用法 182 | 183 | ### 1. 设置请求超时 184 | 185 | ```go 186 | //新建http客户端 187 | client:=httpc.NewHttpClient() 188 | //设置请求超时,默认值为30秒 189 | client.SetTimeout(5*time.Second) 190 | //不设置超时 191 | //client.SetTimeout(0) 192 | //新建一个请求 193 | req:=httpc.NewRequest(client) 194 | req.SetMethod("post").SetUrl("http://127.0.0.1") 195 | //设置头信息,返回byte类型的body 196 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 197 | if err!=nil { 198 | fmt.Println(err) 199 | }else{ 200 | fmt.Println(resp) 201 | fmt.Println(bodyByte) 202 | } 203 | ``` 204 | 205 | ### 2. 设置COOKIE管理器 206 | 207 | ```go 208 | //新建http客户端 209 | client:=httpc.NewHttpClient() 210 | //新建一个cookie管理器,后面所有请求的cookie将保存在这 211 | cookieJar:=httpc.NewCookieJar() 212 | //设置cookie管理器, 213 | client.SetCookieJar(cookieJar) 214 | //新建一个请求 215 | req:=httpc.NewRequest(client) 216 | req.SetMethod("post").SetUrl("http://127.0.0.1") 217 | //设置头信息,返回byte类型的body 218 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 219 | if err!=nil { 220 | fmt.Println(err) 221 | }else{ 222 | //从cookie管理器中获取当前访问url保存的cookie 223 | u, _ := url.Parse("http://127.0.0.1") 224 | cookies:=cookieJar.Cookies(u) 225 | fmt.Println(cookies) 226 | fmt.Println(resp) 227 | fmt.Println(bodyByte) 228 | } 229 | ``` 230 | 231 | ### 3. 设置代理 232 | 233 | ```go 234 | //新建http客户端 235 | client:=httpc.NewHttpClient() 236 | //设置请求代理 237 | client.SetProxy("http://127.0.0.1:10809") 238 | //新建一个请求 239 | req:=httpc.NewRequest(client) 240 | req.SetMethod("post").SetUrl("http://127.0.0.1") 241 | //设置头信息,返回byte类型的body 242 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 243 | if err!=nil { 244 | fmt.Println(err) 245 | }else{ 246 | fmt.Println(resp) 247 | fmt.Println(bodyByte) 248 | } 249 | ``` 250 | 251 | ### 4. 设置重定向处理 252 | 253 | ```go 254 | //新建http客户端 255 | client:=httpc.NewHttpClient() 256 | //设置http客户端重定向处理函数 257 | client.SetRedirect(func(req *http.Request, via []*http.Request) error { 258 | return http.ErrUseLastResponse 259 | }) 260 | //新建一个请求 261 | req:=httpc.NewRequest(client) 262 | req.SetMethod("post").SetUrl("http://127.0.0.1") 263 | //设置头信息,返回byte类型的body 264 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 265 | if err!=nil { 266 | fmt.Println(err) 267 | }else{ 268 | fmt.Println(resp) 269 | fmt.Println(bodyByte) 270 | } 271 | ``` 272 | 273 | ### 5. 设置ssl验证 274 | 275 | ```go 276 | //新建http客户端 277 | client:=httpc.NewHttpClient() 278 | //跳过ssl验证 279 | client.SetSkipVerify(false) 280 | //新建一个请求 281 | req:=httpc.NewRequest(client) 282 | req.SetMethod("post").SetUrl("http://127.0.0.1") 283 | //设置头信息,返回byte类型的body 284 | resp,bodyByte,err:=req.SetHeader("HOST","127.0.0.1").Send().EndByte() 285 | if err!=nil { 286 | fmt.Println(err) 287 | }else{ 288 | fmt.Println(resp) 289 | fmt.Println(bodyByte) 290 | } 291 | ``` 292 | 293 | ## License 294 | 295 | Apache License Version 2.0 see http://www.apache.org/licenses/LICENSE-2.0.html 296 | -------------------------------------------------------------------------------- /body/body.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Body interface { 8 | GetData() string 9 | GetContentType() string 10 | Encode() io.Reader 11 | } -------------------------------------------------------------------------------- /body/form.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "mime/multipart" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | type Form struct { 13 | dataBuf *bytes.Buffer 14 | dataStr string 15 | data *multipart.Writer 16 | } 17 | 18 | func NewFormData() *Form { 19 | bodyBuf := &bytes.Buffer{} 20 | bodyWriter := multipart.NewWriter(bodyBuf) 21 | return &Form{ 22 | dataBuf: bodyBuf, 23 | dataStr: "", 24 | data: bodyWriter, 25 | } 26 | } 27 | 28 | func (this *Form) SetBoundary(boundary string) *Form { 29 | _ = this.data.SetBoundary(boundary) 30 | return this 31 | } 32 | 33 | func (this *Form) SetData(name, value string) *Form { 34 | _ = this.data.WriteField(name, value) 35 | return this 36 | } 37 | 38 | func (this *Form) SetFile(name, file string) *Form { 39 | fd, err := os.Open(file) 40 | defer fd.Close() 41 | if err != nil { 42 | panic("file does not exist") 43 | } 44 | fileWriter, _ := this.data.CreateFormFile(name, filepath.Base(file)) 45 | _, _ = io.Copy(fileWriter, fd) 46 | return this 47 | } 48 | 49 | func (this *Form) GetData() string { 50 | return this.dataStr 51 | } 52 | 53 | func (this *Form) GetContentType() string { 54 | return this.data.FormDataContentType() 55 | } 56 | 57 | func (this *Form) Encode() io.Reader { 58 | _ = this.data.Close() 59 | 60 | r := multipart.NewReader(this.dataBuf, this.data.Boundary()) 61 | f, err := r.ReadForm(0) 62 | if err == nil { 63 | header := "--" + this.data.Boundary() + "\n" 64 | dataStr := header 65 | foot := "--" + this.data.Boundary() + "--" 66 | for k, v := range f.Value { 67 | dataStr += fmt.Sprintf(`Content-Disposition: form-data; name="%s"`, k) + "\n" 68 | dataStr += v[0] + "\n" 69 | } 70 | for fk, fv := range f.File { 71 | dataStr += fmt.Sprintf(`Content-Disposition: form-data; name="%s"; filename="%s" Content-Type: application/octet-stream`, fk, fv[0].Filename) + "\n" 72 | dataStr += fmt.Sprintf("%v", fv) + "\n" 73 | } 74 | dataStr += foot 75 | this.dataStr = dataStr 76 | } 77 | return io.NopCloser(this.dataBuf) 78 | } 79 | -------------------------------------------------------------------------------- /body/raw.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | Text="text/plain" 10 | JavaScript="application/javascript" 11 | Json="application/json" 12 | Html="text/html" 13 | Xml="application/xml" 14 | ) 15 | 16 | type Raw struct { 17 | data string 18 | format string 19 | } 20 | 21 | func NewRawData() *Raw { 22 | return &Raw{ 23 | data: "", 24 | format: "", 25 | } 26 | } 27 | 28 | func (this *Raw) SetData(data,format string) { 29 | this.data=data 30 | this.format=format 31 | } 32 | 33 | func (this *Raw) GetData() string { 34 | return this.data 35 | } 36 | 37 | func (this *Raw) GetContentType() string { 38 | return this.format 39 | } 40 | 41 | func (this *Raw) Encode() io.Reader { 42 | return strings.NewReader(this.data) 43 | } 44 | -------------------------------------------------------------------------------- /body/url.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "io" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | type Url struct { 10 | data *url.Values 11 | } 12 | 13 | func NewUrlEncode() *Url { 14 | return &Url{ 15 | data: &url.Values{}, 16 | } 17 | } 18 | 19 | func (this *Url) SetData(name,value string) *Url { 20 | this.data.Add(name,value) 21 | return this 22 | } 23 | 24 | func (this *Url) GetData() string { 25 | return this.data.Encode() 26 | } 27 | 28 | func (this *Url) GetContentType() string { 29 | return "application/x-www-form-urlencoded" 30 | } 31 | 32 | func (this *Url) Encode() io.Reader { 33 | return strings.NewReader(this.data.Encode()) 34 | } 35 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type HttpClient struct { 11 | client *http.Client 12 | transport *http.Transport 13 | } 14 | 15 | func NewHttpClient() *HttpClient { 16 | tr := &http.Transport{} 17 | 18 | client := &http.Client{ 19 | Transport: tr, 20 | Timeout: 30 * time.Second, 21 | } 22 | return &HttpClient{client: client, transport: tr} 23 | } 24 | 25 | func (this *HttpClient) SetProxy(proxyUrl string) *HttpClient { 26 | proxy, _ := url.Parse(proxyUrl) 27 | this.transport.Proxy = http.ProxyURL(proxy) 28 | return this 29 | } 30 | 31 | func (this *HttpClient) ClearProxy() *HttpClient { 32 | this.transport.Proxy = nil 33 | return this 34 | } 35 | 36 | func (this *HttpClient) SetSkipVerify(isSkipVerify bool) *HttpClient { 37 | this.transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: isSkipVerify} 38 | return this 39 | } 40 | 41 | func (this *HttpClient) SetTimeout(t time.Duration) *HttpClient { 42 | this.client.Timeout = t 43 | return this 44 | } 45 | 46 | func (this *HttpClient) SetCookieJar(j *CookieJar) *HttpClient { 47 | this.client.Jar = j 48 | return this 49 | } 50 | 51 | func (this *HttpClient) SetRedirect(f func(req *http.Request, via []*http.Request) error) *HttpClient { 52 | this.client.CheckRedirect = f 53 | return this 54 | } 55 | -------------------------------------------------------------------------------- /cookiejar.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "sort" 10 | "strings" 11 | "sync" 12 | "time" 13 | "unicode/utf8" 14 | ) 15 | 16 | type CookieJar struct { 17 | mu sync.Mutex 18 | 19 | entries map[string]map[string]entry 20 | 21 | nextSeqNum uint64 22 | } 23 | 24 | func NewCookieJar() *CookieJar { 25 | jar := &CookieJar{ 26 | entries: make(map[string]map[string]entry), 27 | } 28 | 29 | return jar 30 | } 31 | 32 | type entry struct { 33 | Name string 34 | Value string 35 | Domain string 36 | Path string 37 | SameSite string 38 | Secure bool 39 | HttpOnly bool 40 | Persistent bool 41 | HostOnly bool 42 | Expires time.Time 43 | Creation time.Time 44 | LastAccess time.Time 45 | seqNum uint64 46 | } 47 | 48 | func (e *entry) id() string { 49 | return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name) 50 | } 51 | 52 | func (e *entry) shouldSend(https bool, host, path string) bool { 53 | return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure) 54 | } 55 | 56 | func (e *entry) domainMatch(host string) bool { 57 | if e.Domain == host { 58 | return true 59 | } 60 | 61 | return !e.HostOnly && hasDotSuffix(host, e.Domain) 62 | } 63 | 64 | func (e *entry) pathMatch(requestPath string) bool { 65 | if requestPath == e.Path { 66 | return true 67 | } 68 | 69 | if strings.HasPrefix(requestPath, e.Path) { 70 | if e.Path[len(e.Path)-1] == '/' { 71 | return true 72 | } else if requestPath[len(e.Path)] == '/' { 73 | return true 74 | } 75 | } 76 | 77 | return false 78 | } 79 | 80 | func hasDotSuffix(s, suffix string) bool { 81 | return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix 82 | } 83 | 84 | func (j *CookieJar) Cookies(u *url.URL) (cookies []*http.Cookie) { 85 | return j.cookies(u, time.Now()) 86 | } 87 | 88 | func (j *CookieJar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) { 89 | if u.Scheme != "http" && u.Scheme != "https" { 90 | return cookies 91 | } 92 | host, err := canonicalHost(u.Host) 93 | if err != nil { 94 | return cookies 95 | } 96 | 97 | key := jarKey(host) 98 | 99 | j.mu.Lock() 100 | defer j.mu.Unlock() 101 | 102 | submap := j.entries[key] 103 | if submap == nil { 104 | return cookies 105 | } 106 | 107 | https := u.Scheme == "https" 108 | path := u.Path 109 | if path == "" { 110 | path = "/" 111 | } 112 | 113 | modified := false 114 | var selected []entry 115 | for id, e := range submap { 116 | if e.Persistent && !e.Expires.After(now) { 117 | delete(submap, id) 118 | modified = true 119 | continue 120 | } 121 | if !e.shouldSend(https, host, path) { 122 | continue 123 | } 124 | e.LastAccess = now 125 | submap[id] = e 126 | selected = append(selected, e) 127 | modified = true 128 | } 129 | 130 | if modified { 131 | if len(submap) == 0 { 132 | delete(j.entries, key) 133 | } else { 134 | j.entries[key] = submap 135 | } 136 | } 137 | 138 | sort.Slice(selected, func(i, j int) bool { 139 | s := selected 140 | if len(s[i].Path) != len(s[j].Path) { 141 | return len(s[i].Path) > len(s[j].Path) 142 | } 143 | if !s[i].Creation.Equal(s[j].Creation) { 144 | return s[i].Creation.Before(s[j].Creation) 145 | } 146 | return s[i].seqNum < s[j].seqNum 147 | }) 148 | for _, e := range selected { 149 | //修复了读取cookie时缺少Domain,造成读取后的cookie请求失效问题 150 | cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Domain: e.Domain}) 151 | } 152 | 153 | return cookies 154 | } 155 | 156 | func (j *CookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) { 157 | j.setCookies(u, cookies, time.Now()) 158 | } 159 | 160 | func (j *CookieJar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) { 161 | if len(cookies) == 0 { 162 | return 163 | } 164 | 165 | if u.Scheme != "http" && u.Scheme != "https" { 166 | return 167 | } 168 | //There may be errors in setting local domain names, such as:.com.cm 169 | host, err := canonicalHost(u.Host) 170 | if err != nil { 171 | return 172 | } 173 | 174 | key := jarKey(host) 175 | defPath := defaultPath(u.Path) 176 | 177 | j.mu.Lock() 178 | defer j.mu.Unlock() 179 | 180 | submap := j.entries[key] 181 | 182 | modified := false 183 | for _, cookie := range cookies { 184 | e, remove, err := j.newEntry(cookie, now, defPath, host) 185 | if err != nil { 186 | continue 187 | } 188 | id := e.id() 189 | if remove { 190 | if submap != nil { 191 | if _, ok := submap[id]; ok { 192 | delete(submap, id) 193 | modified = true 194 | } 195 | } 196 | continue 197 | } 198 | if submap == nil { 199 | submap = make(map[string]entry) 200 | } 201 | 202 | if old, ok := submap[id]; ok { 203 | e.Creation = old.Creation 204 | e.seqNum = old.seqNum 205 | } else { 206 | e.Creation = now 207 | e.seqNum = j.nextSeqNum 208 | j.nextSeqNum++ 209 | } 210 | e.LastAccess = now 211 | submap[id] = e 212 | modified = true 213 | } 214 | 215 | if modified { 216 | if len(submap) == 0 { 217 | delete(j.entries, key) 218 | } else { 219 | j.entries[key] = submap 220 | } 221 | } 222 | } 223 | 224 | func canonicalHost(host string) (string, error) { 225 | var err error 226 | 227 | host = strings.ToLower(host) 228 | if hasPort(host) { 229 | host, _, err = net.SplitHostPort(host) 230 | if err != nil { 231 | return "", err 232 | } 233 | } 234 | 235 | if strings.HasSuffix(host, ".") { 236 | host = host[:len(host)-1] 237 | } 238 | return toASCII(host) 239 | } 240 | 241 | func hasPort(host string) bool { 242 | colons := strings.Count(host, ":") 243 | if colons == 0 { 244 | return false 245 | } 246 | if colons == 1 { 247 | return true 248 | } 249 | 250 | return host[0] == '[' && strings.Contains(host, "]:") 251 | } 252 | 253 | func jarKey(host string) string { 254 | if isIP(host) { 255 | return host 256 | } 257 | 258 | i := strings.LastIndex(host, ".") 259 | if i <= 0 { 260 | return host 261 | } 262 | 263 | prevDot := strings.LastIndex(host[:i-1], ".") 264 | return host[prevDot+1:] 265 | } 266 | 267 | func isIP(host string) bool { 268 | return net.ParseIP(host) != nil 269 | } 270 | 271 | func defaultPath(path string) string { 272 | /*if len(path) == 0 || path[0] != '/' { 273 | return "/" 274 | } 275 | 276 | i := strings.LastIndex(path, "/") 277 | if i == 0 { 278 | return "/" 279 | } 280 | return path[:i]*/ 281 | //修复了设置cookie时path作用域的问题,统一设置/ 282 | return "/" 283 | } 284 | 285 | func (j *CookieJar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) { 286 | e.Name = c.Name 287 | 288 | if c.Path == "" || c.Path[0] != '/' { 289 | e.Path = defPath 290 | } else { 291 | e.Path = c.Path 292 | } 293 | 294 | e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain) 295 | if err != nil { 296 | return e, false, err 297 | } 298 | 299 | if c.MaxAge < 0 { 300 | return e, true, nil 301 | } else if c.MaxAge > 0 { 302 | e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second) 303 | e.Persistent = true 304 | } else { 305 | if c.Expires.IsZero() { 306 | e.Expires = endOfTime 307 | e.Persistent = false 308 | } else { 309 | if !c.Expires.After(now) { 310 | return e, true, nil 311 | } 312 | e.Expires = c.Expires 313 | e.Persistent = true 314 | } 315 | } 316 | 317 | e.Value = c.Value 318 | e.Secure = c.Secure 319 | e.HttpOnly = c.HttpOnly 320 | 321 | switch c.SameSite { 322 | case http.SameSiteDefaultMode: 323 | e.SameSite = "SameSite" 324 | case http.SameSiteStrictMode: 325 | e.SameSite = "SameSite=Strict" 326 | case http.SameSiteLaxMode: 327 | e.SameSite = "SameSite=Lax" 328 | } 329 | 330 | return e, false, nil 331 | } 332 | 333 | var ( 334 | errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute") 335 | errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute") 336 | errNoHostname = errors.New("cookiejar: no host name available (IP only)") 337 | ) 338 | 339 | var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) 340 | 341 | func (j *CookieJar) domainAndType(host, domain string) (string, bool, error) { 342 | if domain == "" { 343 | return host, true, nil 344 | } 345 | 346 | if isIP(host) { 347 | return "", false, errNoHostname 348 | } 349 | 350 | if domain[0] == '.' { 351 | domain = domain[1:] 352 | } 353 | 354 | if len(domain) == 0 || domain[0] == '.' { 355 | return "", false, errMalformedDomain 356 | } 357 | domain = strings.ToLower(domain) 358 | 359 | if domain[len(domain)-1] == '.' { 360 | return "", false, errMalformedDomain 361 | } 362 | 363 | if host != domain && !hasDotSuffix(host, domain) { 364 | return "", false, errIllegalDomain 365 | } 366 | 367 | return domain, false, nil 368 | } 369 | 370 | const ( 371 | base int32 = 36 372 | damp int32 = 700 373 | initialBias int32 = 72 374 | initialN int32 = 128 375 | skew int32 = 38 376 | tmax int32 = 26 377 | tmin int32 = 1 378 | ) 379 | 380 | func encode(prefix, s string) (string, error) { 381 | output := make([]byte, len(prefix), len(prefix)+1+2*len(s)) 382 | copy(output, prefix) 383 | delta, n, bias := int32(0), initialN, initialBias 384 | b, remaining := int32(0), int32(0) 385 | for _, r := range s { 386 | if r < utf8.RuneSelf { 387 | b++ 388 | output = append(output, byte(r)) 389 | } else { 390 | remaining++ 391 | } 392 | } 393 | h := b 394 | if b > 0 { 395 | output = append(output, '-') 396 | } 397 | for remaining != 0 { 398 | m := int32(0x7fffffff) 399 | for _, r := range s { 400 | if m > r && r >= n { 401 | m = r 402 | } 403 | } 404 | delta += (m - n) * (h + 1) 405 | if delta < 0 { 406 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 407 | } 408 | n = m 409 | for _, r := range s { 410 | if r < n { 411 | delta++ 412 | if delta < 0 { 413 | return "", fmt.Errorf("cookiejar: invalid label %q", s) 414 | } 415 | continue 416 | } 417 | if r > n { 418 | continue 419 | } 420 | q := delta 421 | for k := base; ; k += base { 422 | t := k - bias 423 | if t < tmin { 424 | t = tmin 425 | } else if t > tmax { 426 | t = tmax 427 | } 428 | if q < t { 429 | break 430 | } 431 | output = append(output, encodeDigit(t+(q-t)%(base-t))) 432 | q = (q - t) / (base - t) 433 | } 434 | output = append(output, encodeDigit(q)) 435 | bias = adapt(delta, h+1, h == b) 436 | delta = 0 437 | h++ 438 | remaining-- 439 | } 440 | delta++ 441 | n++ 442 | } 443 | return string(output), nil 444 | } 445 | 446 | func encodeDigit(digit int32) byte { 447 | switch { 448 | case 0 <= digit && digit < 26: 449 | return byte(digit + 'a') 450 | case 26 <= digit && digit < 36: 451 | return byte(digit + ('0' - 26)) 452 | } 453 | panic("cookiejar: internal error in punycode encoding") 454 | } 455 | 456 | func adapt(delta, numPoints int32, firstTime bool) int32 { 457 | if firstTime { 458 | delta /= damp 459 | } else { 460 | delta /= 2 461 | } 462 | delta += delta / numPoints 463 | k := int32(0) 464 | for delta > ((base-tmin)*tmax)/2 { 465 | delta /= base - tmin 466 | k += base 467 | } 468 | return k + (base-tmin+1)*delta/(delta+skew) 469 | } 470 | 471 | const acePrefix = "xn--" 472 | 473 | func toASCII(s string) (string, error) { 474 | if ascii(s) { 475 | return s, nil 476 | } 477 | labels := strings.Split(s, ".") 478 | for i, label := range labels { 479 | if !ascii(label) { 480 | a, err := encode(acePrefix, label) 481 | if err != nil { 482 | return "", err 483 | } 484 | labels[i] = a 485 | } 486 | } 487 | return strings.Join(labels, "."), nil 488 | } 489 | 490 | func ascii(s string) bool { 491 | for i := 0; i < len(s); i++ { 492 | if s[i] >= utf8.RuneSelf { 493 | return false 494 | } 495 | } 496 | return true 497 | } 498 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Albert-Zhan/httpc 2 | 3 | go 1.23 4 | 5 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "compress/gzip" 5 | "context" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "github.com/Albert-Zhan/httpc/body" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | type Request struct { 18 | httpc *HttpClient 19 | request *http.Request 20 | response *http.Response 21 | method string 22 | url string 23 | param *url.Values 24 | header map[string]string 25 | cookies *[]*http.Cookie 26 | data body.Body 27 | debug bool 28 | err error 29 | } 30 | 31 | func NewRequest(client *HttpClient) *Request { 32 | return &Request{ 33 | httpc: client, 34 | method: "GET", 35 | param: &url.Values{}, 36 | header: make(map[string]string), 37 | cookies: new([]*http.Cookie), 38 | debug: false, 39 | err: nil, 40 | } 41 | } 42 | 43 | func (this *Request) SetClient(client *HttpClient) *Request { 44 | this.httpc = client 45 | return this 46 | } 47 | 48 | func (this *Request) SetMethod(name string) *Request { 49 | this.method = strings.ToUpper(name) 50 | return this 51 | } 52 | 53 | func (this *Request) SetUrl(url string) *Request { 54 | this.url = url 55 | return this 56 | } 57 | 58 | func (this *Request) SetParam(name, value string) *Request { 59 | this.param.Add(name, value) 60 | return this 61 | } 62 | 63 | func (this *Request) SetHeader(name, value string) *Request { 64 | this.header[name] = value 65 | return this 66 | } 67 | 68 | func (this *Request) SetBasicAuth(username, password string) *Request { 69 | auth := username + ":" + password 70 | value := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) 71 | this.header["Authorization"] = value 72 | return this 73 | } 74 | 75 | func (this *Request) SetCookies(cookies *[]*http.Cookie) *Request { 76 | this.cookies = cookies 77 | return this 78 | } 79 | 80 | func (this *Request) SetDebug(d bool) *Request { 81 | this.debug = d 82 | return this 83 | } 84 | 85 | func (this *Request) SetBody(body body.Body) *Request { 86 | this.data = body 87 | return this 88 | } 89 | 90 | func (this *Request) Send(ctxs ...context.Context) *Request { 91 | param := this.param.Encode() 92 | if param != "" { 93 | this.url += "?" + param 94 | } 95 | 96 | var data io.Reader 97 | contentType := "" 98 | 99 | if this.data != nil { 100 | data = this.data.Encode() 101 | contentType = this.data.GetContentType() 102 | } 103 | 104 | ctx := context.Background() 105 | if len(ctxs) > 0 { 106 | ctx = ctxs[0] 107 | } 108 | 109 | this.request, this.err = http.NewRequestWithContext(ctx, this.method, this.url, data) 110 | defer this.log() 111 | if this.err != nil { 112 | return this 113 | } 114 | 115 | if contentType != "" { 116 | this.request.Header.Set("Content-Type", contentType) 117 | } 118 | for k, v := range this.header { 119 | this.request.Header.Set(k, v) 120 | } 121 | for _, v := range *this.cookies { 122 | this.request.AddCookie(v) 123 | } 124 | 125 | this.response, this.err = this.httpc.client.Do(this.request) 126 | if this.err != nil { 127 | return this 128 | } 129 | return this 130 | } 131 | 132 | func (this *Request) GetResponse() *http.Response { 133 | return this.response 134 | } 135 | 136 | func (this *Request) GetError() error { 137 | return this.err 138 | } 139 | 140 | func (this *Request) log() { 141 | if this.debug == true { 142 | data := "" 143 | if this.data != nil { 144 | data = this.data.GetData() 145 | } 146 | fmt.Printf("[httpc Debug]\n") 147 | fmt.Printf("-------------------------------------------------------------------\n") 148 | fmt.Printf("Request: %s %s\nHeader: %v\nCookies: %v\n", this.method, this.url, this.request.Header, this.request.Cookies()) 149 | fmt.Printf("Body: %s\n", data) 150 | fmt.Printf("-------------------------------------------------------------------\n") 151 | } 152 | } 153 | 154 | func (this *Request) End() (*http.Response, string, error) { 155 | if this.err != nil { 156 | return nil, "", errors.New(this.err.Error()) 157 | } 158 | 159 | var bodyByte []byte 160 | 161 | if strings.ToLower(this.response.Header.Get("Content-Encoding")) == "gzip" { 162 | reader, _ := gzip.NewReader(this.response.Body) 163 | bodyByte, _ = io.ReadAll(reader) 164 | _ = reader.Close() 165 | } else { 166 | bodyByte, _ = io.ReadAll(this.response.Body) 167 | } 168 | 169 | _ = this.response.Body.Close() 170 | return this.response, string(bodyByte), nil 171 | } 172 | 173 | func (this *Request) EndByte() (*http.Response, []byte, error) { 174 | if this.err != nil { 175 | return nil, []byte(""), errors.New(this.err.Error()) 176 | } 177 | 178 | var bodyByte []byte 179 | 180 | if strings.ToLower(this.response.Header.Get("Content-Encoding")) == "gzip" { 181 | reader, _ := gzip.NewReader(this.response.Body) 182 | bodyByte, _ = io.ReadAll(reader) 183 | _ = reader.Close() 184 | } else { 185 | bodyByte, _ = io.ReadAll(this.response.Body) 186 | } 187 | 188 | _ = this.response.Body.Close() 189 | return this.response, bodyByte, nil 190 | } 191 | 192 | func (this *Request) EndFile(savePath, saveFileName string) (*http.Response, error) { 193 | if this.err != nil { 194 | return nil, errors.New(this.err.Error()) 195 | } 196 | 197 | if this.response.StatusCode != http.StatusOK { 198 | return nil, errors.New("Not written") 199 | } 200 | 201 | if saveFileName == "" { 202 | path := strings.Split(this.request.URL.String(), "/") 203 | if len(path) > 1 { 204 | saveFileName = path[len(path)-1] 205 | } 206 | } 207 | f, err := os.Create(savePath + saveFileName) 208 | if err != nil { 209 | return nil, errors.New(err.Error()) 210 | } 211 | defer f.Close() 212 | 213 | _, err = io.Copy(f, this.response.Body) 214 | _ = this.response.Body.Close() 215 | if err != nil { 216 | return nil, errors.New(err.Error()) 217 | } 218 | 219 | return this.response, nil 220 | } 221 | --------------------------------------------------------------------------------