├── .gitignore
├── docs
└── assets
│ ├── avatar.jpg
│ └── cover.jpg
├── error_test.go
├── go.mod
├── .travis.yml
├── benchmark_test.go
├── LICENSE
├── formdata.go
├── request.go
├── error.go
├── README.md
├── datatype_test.go
├── api_test.go
├── response_test.go
├── api.go
├── download_test.go
├── download.go
├── session_test.go
├── response.go
├── go.sum
├── datatype.go
└── session.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | *.DS_Store
3 | *.idea
4 |
--------------------------------------------------------------------------------
/docs/assets/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wnanbei/direwolf/HEAD/docs/assets/avatar.jpg
--------------------------------------------------------------------------------
/docs/assets/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wnanbei/direwolf/HEAD/docs/assets/cover.jpg
--------------------------------------------------------------------------------
/error_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | )
7 |
8 | func TestError(t *testing.T) {
9 | err1 := ErrRequestBody
10 | err2 := WrapErr(err1, "second testing")
11 | err3 := WrapErrf(err2, "==%s==", "third testing")
12 |
13 | if !errors.Is(err3, ErrRequestBody) {
14 | t.Fatal("Test errors.Is failed.")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/wnanbei/direwolf
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/PuerkitoBio/goquery v1.5.0
7 | github.com/andybalholm/cascadia v1.1.0 // indirect
8 | github.com/gin-gonic/gin v1.7.7
9 | github.com/json-iterator/go v1.1.9
10 | github.com/tidwall/gjson v1.14.0
11 | github.com/valyala/fasthttp v1.35.0
12 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f
13 | golang.org/x/text v0.3.7
14 | )
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | os:
4 | - linux
5 | - osx
6 |
7 | go:
8 | - 1.13.x
9 |
10 | matrix:
11 | fast_finish: true
12 |
13 | script:
14 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic
15 |
16 | after_success:
17 | - bash <(curl -s https://codecov.io/bash)
18 |
19 | notifications:
20 | email:
21 | recipients:
22 | - wnanbei@gmail.com
23 | on_success: never # default: change
24 | on_failure: always # default: always
--------------------------------------------------------------------------------
/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/valyala/fasthttp"
9 | )
10 |
11 | func BenchmarkDirewolfGet(b *testing.B) {
12 | ts := newTestResponseServer()
13 | defer ts.Close()
14 | b.ResetTimer()
15 | for i := 0; i < b.N; i++ {
16 | resp, _ := Get(ts.URL)
17 | resp.Text()
18 | }
19 | }
20 |
21 | func BenchmarkGolangGet(b *testing.B) {
22 | ts := newTestResponseServer()
23 | defer ts.Close()
24 | b.ResetTimer()
25 | for i := 0; i < b.N; i++ {
26 | resp, _ := http.Get(ts.URL)
27 | ioutil.ReadAll(resp.Body)
28 | resp.Body.Close()
29 | }
30 | }
31 |
32 | func BenchmarkFasthttpGet(b *testing.B) {
33 | ts := newTestResponseServer()
34 | defer ts.Close()
35 | b.ResetTimer()
36 | client := fasthttp.Client{}
37 | for i := 0; i < b.N; i++ {
38 | req := fasthttp.AcquireRequest()
39 | resp := fasthttp.AcquireResponse()
40 | req.SetRequestURI(ts.URL)
41 | client.Do(req, resp)
42 | req.Reset()
43 | resp.Reset()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 wnanbei
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/formdata.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "mime/multipart"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | type MultipartForm struct {
12 | body *bytes.Buffer
13 | m *multipart.Writer
14 | }
15 |
16 | func NewMultipartForm() *MultipartForm {
17 | body := bytes.Buffer{}
18 | w := multipart.NewWriter(&body)
19 |
20 | mf := MultipartForm{
21 | body: &body,
22 | m: w,
23 | }
24 | return &mf
25 | }
26 |
27 | func (mf *MultipartForm) WriteField(key, value string) error {
28 | return mf.m.WriteField(key, value)
29 | }
30 |
31 | func (mf *MultipartForm) WriteFile(key, filePath string) error {
32 | _, fileName := filepath.Split(filePath)
33 |
34 | fw, err := mf.m.CreateFormFile(key, fileName)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | f, err := os.Open(filePath)
40 | if err != nil {
41 | return err
42 | }
43 | defer f.Close()
44 |
45 | _, err = io.Copy(fw, f)
46 | return err
47 | }
48 |
49 | func (mf *MultipartForm) Close() error {
50 | return mf.m.Close()
51 | }
52 |
53 | func (mf *MultipartForm) Reader() *bytes.Reader {
54 | return bytes.NewReader(mf.body.Bytes())
55 | }
56 |
57 | func (mf *MultipartForm) ContentType() string {
58 | return mf.m.FormDataContentType()
59 | }
60 |
61 | func (mf *MultipartForm) bindRequest(request *Request) error {
62 | request.MultipartForm = mf
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/request.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | )
7 |
8 | // Request is a prepared request setting, you should construct it by using
9 | // NewRequest().
10 | type Request struct {
11 | Method string
12 | URL string
13 | Headers http.Header
14 | Body []byte
15 | JsonBody []byte
16 | Params *Params
17 | PostForm *PostForm
18 | Cookies Cookies
19 | Proxy *Proxy
20 | RedirectNum int
21 | Timeout int
22 | MultipartForm *MultipartForm
23 | }
24 |
25 | // NewRequest construct a Request by passing the parameters.
26 | //
27 | // You can construct this request by passing the following parameters:
28 | // method: Method for the request.
29 | // url: URL for the request.
30 | // direwolf.Header: HTTP Headers to send.
31 | // direwolf.Params: Parameters to send in the query string.
32 | // direwolf.Cookies: Cookies to send.
33 | // direwolf.PostForm: Post data form to send.
34 | // direwolf.Body: Post body to send.
35 | // direwolf.Proxy: Proxy url to use.
36 | // direwolf.Timeout: Request Timeout.
37 | // direwolf.RedirectNum: Number of Request allowed to redirect.
38 | func NewRequest(method string, URL string, args ...RequestOption) (req *Request, err error) {
39 | req = &Request{} // new a Request and set default field
40 | req.Method = strings.ToUpper(method) // Upper the method string
41 | req.URL = URL
42 |
43 | // Check the type of the parameter and handle it.
44 | for _, arg := range args {
45 | if err := arg.bindRequest(req); err != nil {
46 | return nil, WrapErr(err, "set request parameters failed.")
47 | }
48 | }
49 | return req, nil
50 | }
51 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "runtime"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | var (
12 | ErrRequestBody = errors.New("request body can`t coexists with PostForm")
13 | ErrTimeout = errors.New("reqeust timeout")
14 | )
15 |
16 | type RedirectError struct {
17 | RedirectNum int
18 | }
19 |
20 | func (e *RedirectError) Error() string {
21 | return "exceeded the maximum number of redirects: " + strconv.Itoa(e.RedirectNum)
22 | }
23 |
24 | type Error struct {
25 | // wrapped error
26 | err error
27 | msg string
28 | // file path and name
29 | file string
30 | fileLine int
31 | time string
32 | }
33 |
34 | func (e *Error) Error() string {
35 | _, ok := e.err.(interface {
36 | Unwrap() error
37 | })
38 | if ok {
39 | return fmt.Sprintf("%s - %s:%d\n%s\n%s", e.time, e.file, e.fileLine, e.msg, e.err.Error())
40 | }
41 | return fmt.Sprintf("%s - %s:%d\n%s\n\n%s\n", e.time, e.file, e.fileLine, e.msg, e.err.Error())
42 | }
43 |
44 | func (e *Error) Unwrap() error {
45 | if e.err != nil {
46 | return e.err
47 | }
48 | return nil
49 | }
50 |
51 | // WrapErr will wrap a error with some information: filename, line, time and some message.
52 | func WrapErr(err error, msg string) error {
53 | _, file, line, _ := runtime.Caller(1)
54 | return &Error{
55 | err: err,
56 | msg: msg,
57 | file: file,
58 | fileLine: line,
59 | time: time.Now().Format("2006-01-02 15:04:05"),
60 | }
61 | }
62 |
63 | // WrapErrf will wrap a error with some information: filename, line, time and some message.
64 | // You can format message of error.
65 | func WrapErrf(err error, format string, args ...interface{}) error {
66 | msg := fmt.Sprintf(format, args...)
67 | _, file, line, _ := runtime.Caller(1)
68 | return &Error{
69 | err: err,
70 | msg: msg,
71 | file: file,
72 | fileLine: line,
73 | time: time.Now().Format("2006-01-02 15:04:05"),
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Direwolf HTTP Client: Save your time
2 |
3 | [](https://travis-ci.org/wnanbei/direwolf)
4 | [](https://codecov.io/gh/wnanbei/direwolf)
5 | 
6 | 
7 | 
8 |
9 | Package direwolf is a convenient and easy to use http client written in Golang.
10 |
11 | If you want find more info, please go here: [Direwolf HTTP Client: Save your time](https://wnanbei.github.io/direwolf/),内有中文文档。
12 |
13 | 
14 |
15 | ## Feature Support
16 |
17 | - Clean and Convenient API
18 | - Simple to Set Headers, Cookies, Parameters, Post Forms
19 | - Sessions Control
20 | - Keep-Alive & Connection Pooling
21 | - HTTP(S) Proxy Support
22 | - Redirect Control
23 | - Timeout Control
24 | - Support extract result from response body with css selector, regexp, json
25 | - Content Decoding
26 | - More to come...
27 |
28 | ## Installation
29 |
30 | ```text
31 | go get github.com/wnanbei/direwolf
32 | ```
33 |
34 | ## Take a glance
35 |
36 | You can easily send a request like this:
37 |
38 | ```go
39 | import (
40 | "fmt"
41 |
42 | dw "github.com/wnanbei/direwolf"
43 | )
44 |
45 | func main() {
46 | resp, err := dw.Get("https://www.google.com")
47 | if err != nil {
48 | return
49 | }
50 | fmt.Println(resp.Text())
51 | }
52 | ```
53 |
54 | Besides, direwolf provide a convenient way to add parameters to request. Such
55 | as Headers, Cookies, Params, etc.
56 |
57 | ```go
58 | import (
59 | "fmt"
60 |
61 | dw "github.com/wnanbei/direwolf"
62 | )
63 |
64 | func main() {
65 | headers := dw.NewHeaders(
66 | "User-Agent", "direwolf",
67 | )
68 | params := dw.NewParams(
69 | "name", "wnanbei",
70 | "age", "18",
71 | )
72 | cookies := dw.NewCookies(
73 | "sign", "kzhxciuvyqwekhiuxcyvnkjdhiue",
74 | )
75 | resp, err := dw.Get("https://httpbin.org/get", headers, params, cookies)
76 | if err != nil {
77 | return
78 | }
79 | fmt.Println(resp.Text())
80 | }
81 | ```
82 |
83 | Output:
84 |
85 | ```json
86 | {
87 | "args": {
88 | "age": "18",
89 | "name": "wnanbei"
90 | },
91 | "headers": {
92 | "Accept-Encoding": "gzip",
93 | "Cookie": "sign=kzhxciuvyqwekhiuxcyvnkjdhiue",
94 | "Host": "httpbin.org",
95 | "User-Agent": "direwolf"
96 | },
97 | "origin": "1.1.1.1, 1.1.1.1",
98 | "url": "https://httpbin.org/get?age=18&name=wnanbei"
99 | }
100 | ```
101 |
102 | ## Contribute
103 |
104 | Direwolf is a personal project now, but all contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome.
105 |
106 | If you find a bug in direwolf or have some good ideas:
107 |
108 | - Go to [GitHub “issues” tab](https://github.com/wnanbei/direwolf/issues) and open a fresh issue to start a discussion around a feature idea or a bug.
109 | - Send a pull request and bug the maintainer until it gets merged and published.
110 | - Write a test which shows that the bug was fixed or that the feature works as expected.
111 |
112 | If you need to discuss about direwolf with me, you can send me a e-mail.
113 |
--------------------------------------------------------------------------------
/datatype_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func TestHeaders(t *testing.T) {
13 | headers := NewHeaders(
14 | "key1", "value1",
15 | "key2", "value2",
16 | )
17 | if headers.Get("key1") != "value1" {
18 | t.Fatal("Headers.Get() failed.")
19 | }
20 | if headers.Get("key3") != "" {
21 | t.Fatal("Headers.Get() failed.")
22 | }
23 | }
24 |
25 | func TestStringSliceMap(t *testing.T) {
26 | params := NewParams(
27 | "key1", "value1",
28 | "key2", "value2",
29 | )
30 | if params.Get("key1") != "value1" {
31 | t.Fatal("params.Get() failed.")
32 | }
33 | if params.Get("key3") != "" {
34 | t.Fatal("params.Get() failed.")
35 | }
36 |
37 | if params.URLEncode() != "key1=value1&key2=value2" {
38 | t.Fatal("params.URLEncode() failed.")
39 | }
40 |
41 | params.Add("key1", "value3")
42 | if params.Get("key1", 1) != "value3" {
43 | t.Fatal("params.Add() failed.")
44 | }
45 |
46 | params.Set("key1", "value4")
47 | if params.Get("key1") != "value4" {
48 | t.Fatal("params.Set() failed.")
49 | }
50 |
51 | params.Del("key2")
52 | if params.Get("key2") != "" {
53 | t.Fatal("params.Del() failed.")
54 | }
55 |
56 | postForm := NewPostForm(
57 | "key1", "value1",
58 | "key2", "value2",
59 | )
60 | if postForm.Get("key1") != "value1" {
61 | t.Fatal("PostForm.Get() failed.")
62 | }
63 | }
64 |
65 | func TestParams(t *testing.T) {
66 | URL := "http://test.com"
67 | params := NewParams(
68 | "key1", "value2",
69 | "key2", "value2")
70 | req, err := NewRequest("GET", URL, params)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | if req.URL != "http://test.com?key1=value2&key2=value2" {
75 | t.Fatal("Test params failed.")
76 | }
77 |
78 | URL = "http://test.com?"
79 | params = NewParams(
80 | "key1", "value2",
81 | "key2", "value2")
82 | req, err = NewRequest("GET", URL, params)
83 | if err != nil {
84 | t.Fatal(err)
85 | }
86 | if req.URL != "http://test.com?key1=value2&key2=value2" {
87 | t.Fatal("Test params failed.")
88 | }
89 |
90 | URL = "http://test.com?xxx=yyy"
91 | params = NewParams(
92 | "key1", "value2",
93 | "key2", "value2")
94 | req, err = NewRequest("GET", URL, params)
95 | if err != nil {
96 | t.Fatal(err)
97 | }
98 | if req.URL != "http://test.com?xxx=yyy&key1=value2&key2=value2" {
99 | t.Fatal("Test params failed.")
100 | }
101 | }
102 |
103 | func GetJsonTestServer() *httptest.Server {
104 | gin.SetMode(gin.ReleaseMode)
105 | router := gin.New()
106 | router.GET("/json", func(c *gin.Context) {
107 | data, err := ioutil.ReadAll(c.Request.Body)
108 | if err != nil {
109 | fmt.Println(err)
110 | }
111 | c.String(200, string(data))
112 | })
113 | ts := httptest.NewServer(router)
114 | return ts
115 | }
116 |
117 | func TestJsonBody(t *testing.T) {
118 | ts := GetJsonTestServer()
119 |
120 | type Student struct {
121 | Name string
122 | Age int
123 | Guake bool
124 | }
125 | jsonText := &Student{
126 | "Xiao Ming",
127 | 16,
128 | true,
129 | }
130 |
131 | jsonBody := NewJsonBody(jsonText)
132 | resp, err := Get(ts.URL+"/json", jsonBody)
133 | if err != nil {
134 | t.Fatal("TestJsonBody Failed.")
135 | }
136 |
137 | if resp.JsonGet("Name").String() != "Xiao Ming" {
138 | t.Fatal("TestJsonBody Failed.")
139 | }
140 |
141 | jsonRespBody := &Student{}
142 | if err := resp.Json(jsonRespBody); err != nil {
143 | t.Fatal("TestJsonBody Failed.")
144 | }
145 | if jsonRespBody.Name != "Xiao Ming" {
146 | t.Fatal("TestJsonBody Failed.")
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/api_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestGet(t *testing.T) {
8 | ts := newTestSessionServer()
9 | defer ts.Close()
10 |
11 | resp, err := Get(ts.URL + "/test")
12 | if err != nil {
13 | t.Fatal(err)
14 | }
15 | text := resp.Text()
16 | if text != "GET" {
17 | t.Fatal("Get test failed")
18 | }
19 | }
20 |
21 | func TestPost(t *testing.T) {
22 | ts := newTestSessionServer()
23 | defer ts.Close()
24 |
25 | postForm := NewPostForm(
26 | "key", "value",
27 | )
28 | resp, err := Post(ts.URL+"/test", postForm)
29 | if err != nil {
30 | t.Fatal(err)
31 | }
32 | text := resp.Text()
33 | if text != "POST" {
34 | t.Fatal("Post test failed")
35 | }
36 |
37 | body := Body("key=value")
38 | resp2, err := Post(ts.URL+"/test", body)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | text2 := resp2.Text()
43 | if text2 != "POST" {
44 | t.Fatal("Post test failed")
45 | }
46 | }
47 |
48 | func TestPut(t *testing.T) {
49 | ts := newTestSessionServer()
50 | defer ts.Close()
51 |
52 | resp, err := Put(ts.URL)
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | text := resp.Text()
57 | if text != "PUT" {
58 | t.Fatal("Put test failed")
59 | }
60 | }
61 |
62 | func TestPatch(t *testing.T) {
63 | ts := newTestSessionServer()
64 | defer ts.Close()
65 |
66 | resp, err := Patch(ts.URL)
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | text := resp.Text()
71 | if text != "PATCH" {
72 | t.Fatal("Patch test failed")
73 | }
74 | }
75 |
76 | func TestDelete(t *testing.T) {
77 | ts := newTestSessionServer()
78 | defer ts.Close()
79 |
80 | resp, err := Delete(ts.URL)
81 | if err != nil {
82 | t.Fatal(err)
83 | }
84 | text := resp.Text()
85 | if text != "DELETE" {
86 | t.Fatal("Delete test failed")
87 | }
88 | }
89 |
90 | func TestHead(t *testing.T) {
91 | ts := newTestSessionServer()
92 | defer ts.Close()
93 |
94 | resp, err := Head(ts.URL)
95 | if err != nil {
96 | t.Fatal(err)
97 | }
98 | cookies := resp.Cookies
99 | if cookies[0].Name != "HEAD" {
100 | t.Fatal("Head test failed")
101 | }
102 | }
103 |
104 | func TestRequest(t *testing.T) {
105 | ts := newTestSessionServer()
106 | defer ts.Close()
107 |
108 | req, err := NewRequest("Get", ts.URL+"/test")
109 | if err != nil {
110 | t.Fatal(err)
111 | }
112 |
113 | resp, err := Send(req)
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 | text := resp.Text()
118 | if text != "GET" {
119 | t.Fatal("Request test failed")
120 | }
121 | }
122 |
123 | func TestSendCookie(t *testing.T) {
124 | ts := newTestSessionServer()
125 | defer ts.Close()
126 |
127 | cookies := NewCookies(
128 | "name", "direwolf",
129 | )
130 | resp, err := Get(ts.URL+"/getCookie", cookies)
131 | if err != nil {
132 | t.Fatal(err)
133 | }
134 | if resp.Text() != "name=direwolf" {
135 | t.Fatal("request cookies test failed")
136 | }
137 | }
138 |
139 | func TestSendHeaders(t *testing.T) {
140 | ts := newTestSessionServer()
141 | defer ts.Close()
142 |
143 | headers := NewHeaders(
144 | "User-Agent", "direwolf",
145 | )
146 | resp, err := Get(ts.URL+"/getHeader", headers)
147 | if err != nil {
148 | t.Fatal(err)
149 | }
150 | text := resp.Text()
151 | if text != "direwolf" {
152 | t.Fatal("request headers test failed")
153 | }
154 | }
155 |
156 | func TestSendParams(t *testing.T) {
157 | ts := newTestSessionServer()
158 | defer ts.Close()
159 |
160 | params := NewParams(
161 | "key", "value",
162 | )
163 | resp, err := Get(ts.URL+"/getParams", params)
164 | if err != nil {
165 | t.Fatal(err)
166 | }
167 | if resp.Text() != "value" {
168 | t.Fatal("request params test failed")
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/response_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net/http/httptest"
5 | "testing"
6 |
7 | "github.com/gin-gonic/gin"
8 |
9 | "golang.org/x/text/encoding/charmap"
10 | "golang.org/x/text/encoding/simplifiedchinese"
11 | )
12 |
13 | func newTestResponseServer() *httptest.Server {
14 | respString := `
15 |
16 | Direwolf
17 |
18 |
19 | is a convenient
20 | and easy to use http client with Golang
21 | 南北
22 | 2019-06-21
23 |
24 | `
25 |
26 | gin.SetMode(gin.ReleaseMode)
27 | router := gin.New()
28 | router.GET("/", func(c *gin.Context) {
29 | c.String(200, respString)
30 | })
31 | router.GET("/GBK", func(c *gin.Context) {
32 | content, _ := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(respString))
33 | c.Data(200, "text/html", content)
34 | })
35 | router.GET("/GB18030", func(c *gin.Context) {
36 | content, _ := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(respString))
37 | c.Data(200, "text/html", content)
38 | })
39 | router.GET("/latin1", func(c *gin.Context) {
40 | content, _ := charmap.ISO8859_1.NewEncoder().Bytes([]byte(`...`))
41 | c.Data(200, "text/html", content)
42 | })
43 | ts := httptest.NewServer(router)
44 | return ts
45 | }
46 |
47 | func TestReExtract(t *testing.T) {
48 | ts := newTestResponseServer()
49 | defer ts.Close()
50 |
51 | resp, err := Get(ts.URL)
52 | if err != nil {
53 | t.Fatal(err)
54 | }
55 | result1 := resp.Re(`\d{4}-\d{2}-\d{2}`)
56 | if result1[0] != "2019-06-21" {
57 | t.Fatal("Response.Re() failed.")
58 | }
59 |
60 | result2 := resp.ReSubMatch(`(.*?)`)
61 | if len(result2) != 4 {
62 | t.Fatal("Response.ReSubMatch() failed.")
63 | }
64 | if result2[3][0] != "2019-06-21" {
65 | t.Fatal("Response.ReSubMatch() failed.")
66 | }
67 | }
68 |
69 | func TestCssExtract(t *testing.T) {
70 | ts := newTestResponseServer()
71 | defer ts.Close()
72 |
73 | resp, err := Get(ts.URL)
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 | result1 := resp.CSS(`a`).First().Text()
78 | if result1 != "is a convenient" {
79 | t.Fatal("Response.CSS().First().Text() failed.")
80 | }
81 |
82 | result2 := resp.CSS(`body`).CSS(`li`).CSS(`a[href=\/time\/]`).First().Text()
83 | if result2 != "2019-06-21" {
84 | t.Fatal("Response.CSS() failed.")
85 | }
86 |
87 | result3 := resp.CSS(`a`).First().Attr("href")
88 | if result3 != "/convenient/" {
89 | t.Fatal("Response.CSS().First().Attr() failed.")
90 | }
91 |
92 | result5 := resp.CSS(`a`).At(2).Text()
93 | if result5 != "南北" {
94 | t.Fatal("Response.CSS().At() failed.")
95 | }
96 | }
97 |
98 | func TestResponseEncoding(t *testing.T) {
99 | ts := newTestResponseServer()
100 | defer ts.Close()
101 |
102 | resp3, err := Get(ts.URL + "/latin1")
103 | if err != nil {
104 | t.Fatal(err)
105 | }
106 | resp3.Encoding("latin1")
107 | result3 := resp3.ReSubMatch(`(.*?)`)
108 | if result3[0][0] != "..." {
109 | t.Fatal("Response latin1 failed.")
110 | }
111 |
112 | resp, err := Get(ts.URL + "/GBK")
113 | if err != nil {
114 | t.Fatal(err)
115 | }
116 | resp.Encoding("GBK")
117 | result1 := resp.ReSubMatch(`(.*?)`)
118 | if result1[0][0] != "南北" {
119 | t.Fatal("Response GBK failed.")
120 | }
121 |
122 | resp2, err := Get(ts.URL + "/GB18030")
123 | if err != nil {
124 | t.Fatal(err)
125 | }
126 | resp2.Encoding("GB18030")
127 | result2 := resp2.ReSubMatch(`(.*?)`)
128 | if result2[0][0] != "南北" {
129 | t.Fatal("Response GB18030 failed.")
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/api.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package direwolf is a convenient and easy to use http client written in Golang.
3 | */
4 | package direwolf
5 |
6 | // Default global session
7 | var defaultSession *Session
8 |
9 | func init() {
10 | sessionOptions := DefaultSessionOptions() // New default global session
11 | sessionOptions.DisableCookieJar = true
12 | defaultSession = NewSession(sessionOptions)
13 | }
14 |
15 | // Send is different with Get and Post method, you should pass a
16 | // Request to it. You can construct Request by use NewRequest
17 | // method.
18 | func Send(req *Request) (*Response, error) {
19 | resp, err := defaultSession.Send(req)
20 | if err != nil {
21 | return nil, err
22 | }
23 | return resp, nil
24 | }
25 |
26 | // Get is the most common method of direwolf to constructs and sends a
27 | // Get request.
28 | //
29 | // You can construct this request by passing the following parameters:
30 | // url: URL for the request.
31 | // http.Header: HTTP Headers to send.
32 | // direwolf.Params: Parameters to send in the query string.
33 | // direwolf.Cookies: Cookies to send.
34 | // direwolf.PostForm: Post data form to send.
35 | // direwolf.Body: Post body to send.
36 | // direwolf.Proxy: Proxy url to use.
37 | // direwolf.Timeout: Request Timeout. Default value is 30.
38 | // direwolf.RedirectNum: Number of Request allowed to redirect. Default value is 5.
39 | func Get(URL string, args ...RequestOption) (*Response, error) {
40 | req, err := NewRequest("GET", URL, args...)
41 | if err != nil {
42 | return nil, err
43 | }
44 | resp, err := Send(req)
45 | if err != nil {
46 | return nil, err
47 | }
48 | return resp, nil
49 | }
50 |
51 | // Post is the method to constructs and sends a Post request. Parameters are
52 | // the same with direwolf.Get()
53 | //
54 | // Note: direwolf.Body can`t existed with direwolf.PostForm.
55 | func Post(URL string, args ...RequestOption) (*Response, error) {
56 | req, err := NewRequest("POST", URL, args...)
57 | if err != nil {
58 | return nil, err
59 | }
60 | resp, err := Send(req)
61 | if err != nil {
62 | return nil, err
63 | }
64 | return resp, nil
65 | }
66 |
67 | // Head is the method to constructs and sends a Head request. Parameters are
68 | // the same with direwolf.Get()
69 | func Head(URL string, args ...RequestOption) (*Response, error) {
70 | req, err := NewRequest("HEAD", URL, args...)
71 | if err != nil {
72 | return nil, err
73 | }
74 | resp, err := Send(req)
75 | if err != nil {
76 | return nil, err
77 | }
78 | return resp, nil
79 | }
80 |
81 | // Put is the method to constructs and sends a Put request. Parameters are
82 | // the same with direwolf.Get()
83 | func Put(URL string, args ...RequestOption) (*Response, error) {
84 | req, err := NewRequest("Put", URL, args...)
85 | if err != nil {
86 | return nil, err
87 | }
88 | resp, err := Send(req)
89 | if err != nil {
90 | return nil, err
91 | }
92 | return resp, nil
93 | }
94 |
95 | // Patch is the method to constructs and sends a Patch request. Parameters are
96 | // the same with direwolf.Get()
97 | func Patch(URL string, args ...RequestOption) (*Response, error) {
98 | req, err := NewRequest("Patch", URL, args...)
99 | if err != nil {
100 | return nil, err
101 | }
102 | resp, err := Send(req)
103 | if err != nil {
104 | return nil, err
105 | }
106 | return resp, nil
107 | }
108 |
109 | // Delete is the method to constructs and sends a Delete request. Parameters are
110 | // the same with direwolf.Get()
111 | func Delete(URL string, args ...RequestOption) (*Response, error) {
112 | req, err := NewRequest("Delete", URL, args...)
113 | if err != nil {
114 | return nil, err
115 | }
116 | resp, err := Send(req)
117 | if err != nil {
118 | return nil, err
119 | }
120 | return resp, nil
121 | }
122 |
--------------------------------------------------------------------------------
/download_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "net/http/httptest"
9 | "strconv"
10 | "testing"
11 | "time"
12 |
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | func newTestProxyServer() *httptest.Server {
17 | gin.SetMode(gin.ReleaseMode)
18 | router := gin.New()
19 | router.GET("/:a", func(c *gin.Context) {
20 | reqUrl := c.Request.URL.String() // Get request url
21 | req, err := http.NewRequest(c.Request.Method, reqUrl, nil)
22 | if err != nil {
23 | c.AbortWithStatus(404)
24 | return
25 | }
26 |
27 | // Forwarding requests from client.
28 | cli := &http.Client{}
29 | resp, err := cli.Do(req)
30 | if err != nil {
31 | c.AbortWithStatus(404)
32 | return
33 | }
34 | defer resp.Body.Close()
35 |
36 | body, err := ioutil.ReadAll(resp.Body)
37 | c.Data(200, "text/plain", body) // write response body to response.
38 | c.String(200, "This is proxy Server.") // add proxy info.
39 | })
40 | ts := httptest.NewServer(router)
41 | return ts
42 | }
43 |
44 | func TestSetProxy(t *testing.T) {
45 | proxyServer := newTestProxyServer()
46 | defer proxyServer.Close()
47 | targetServer := newTestSessionServer()
48 | defer targetServer.Close()
49 |
50 | proxy := &Proxy{
51 | HTTP: proxyServer.URL,
52 | HTTPS: proxyServer.URL,
53 | }
54 | resp, err := Get(targetServer.URL+"/proxy", proxy)
55 | if err != nil {
56 | t.Fatal("SetProxy failed: ", err)
57 | }
58 | if resp.Text() != "This is target website.This is proxy Server." {
59 | t.Fatal("SetProxy failed: ", err)
60 | }
61 | }
62 |
63 | func newTestTimeoutServer() *httptest.Server {
64 | gin.SetMode(gin.ReleaseMode)
65 | router := gin.New()
66 | router.GET("/", func(c *gin.Context) {
67 | time.Sleep(time.Second * 2)
68 | c.String(200, "successed")
69 | })
70 | ts := httptest.NewServer(router)
71 | return ts
72 | }
73 |
74 | func TestTimeout(t *testing.T) {
75 | timeoutServer := newTestTimeoutServer()
76 | defer timeoutServer.Close()
77 |
78 | _, err := Get(timeoutServer.URL, Timeout(1))
79 | if err != nil {
80 | if !errors.Is(err, ErrTimeout) {
81 | t.Fatal("TestTimeout failed: ", err)
82 | }
83 | }
84 |
85 | _, err = Get(timeoutServer.URL, Timeout(3))
86 | if err != nil {
87 | t.Fatal("TestTimeout failed: ", err)
88 | }
89 | }
90 |
91 | func newTestRedirectServer() *httptest.Server {
92 | gin.SetMode(gin.ReleaseMode)
93 | router := gin.New()
94 | router.GET("/", func(c *gin.Context) {
95 | c.String(200, "successed")
96 | })
97 | router.GET("/:number", func(c *gin.Context) {
98 | number, _ := strconv.Atoi(c.Param("number"))
99 | if number == 1 {
100 | c.Redirect(302, "/")
101 | }
102 | number = number - 1
103 | c.Redirect(302, strconv.Itoa(number))
104 | })
105 | ts := httptest.NewServer(router)
106 | return ts
107 | }
108 |
109 | func TestRedirect(t *testing.T) {
110 | redirectServer := newTestRedirectServer()
111 | defer redirectServer.Close()
112 |
113 | redirect := RedirectNum(-1) // ban redirect
114 | resp, err := Get(redirectServer.URL+"/", redirect)
115 | if err != nil {
116 | t.Fatal("Test TestRedirectError failed.")
117 | }
118 | if resp.Text() != "successed" {
119 | t.Fatal("Test TestRedirectError failed.")
120 | }
121 |
122 | redirect = RedirectNum(1) // allow 1 redirect
123 | resp, err = Get(redirectServer.URL+"/1", redirect)
124 | if err != nil {
125 | t.Fatal("Test TestRedirectError failed.")
126 | }
127 | if resp.Text() != "successed" {
128 | t.Fatal("Test TestRedirectError failed.")
129 | }
130 |
131 | redirect = RedirectNum(1) // allow 1 redirect
132 | resp, err = Get(redirectServer.URL+"/2", redirect)
133 | if err != nil {
134 | var errType *RedirectError
135 | if !errors.As(err, &errType) { // check RedirectError
136 | fmt.Println("yyyyy")
137 | t.Fatal("Test TestRedirectError failed.")
138 | }
139 | } else {
140 | t.Fatal("Test TestRedirectError failed.")
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/download.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "io"
8 | "io/ioutil"
9 | "net/http"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // send is low level request method.
15 | func send(session *Session, req *Request) (*Response, error) {
16 | // Set timeout to request context.
17 | // Default timeout is 30s.
18 | timeout := time.Second * 30
19 | if req.Timeout > 0 {
20 | timeout = time.Second * time.Duration(req.Timeout)
21 | } else if session.Timeout > 0 {
22 | timeout = time.Second * time.Duration(session.Timeout)
23 | }
24 | ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
25 |
26 | // set proxy to request context.
27 | if req.Proxy != nil {
28 | ctx = context.WithValue(ctx, "http", req.Proxy.HTTP)
29 | ctx = context.WithValue(ctx, "https", req.Proxy.HTTPS)
30 | } else if session.Proxy != nil {
31 | ctx = context.WithValue(ctx, "http", session.Proxy.HTTP)
32 | ctx = context.WithValue(ctx, "https", session.Proxy.HTTPS)
33 | }
34 |
35 | // set RedirectNum to request context.
36 | // default RedirectNum is 10.
37 | if req.RedirectNum == 0 {
38 | ctx = context.WithValue(ctx, "redirectNum", 10)
39 | } else if req.RedirectNum > 0 {
40 | ctx = context.WithValue(ctx, "redirectNum", req.RedirectNum)
41 | } else {
42 | ctx = context.WithValue(ctx, "redirectNum", 0)
43 | }
44 |
45 | // Make new http.Request with context
46 | httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL, nil)
47 | if err != nil {
48 | timeoutCancel()
49 | return nil, WrapErr(err, "build Request error, please check request url or request method")
50 | }
51 |
52 | // Handle the Headers.
53 | httpReq.Header = mergeHeaders(req.Headers, session.Headers)
54 |
55 | // Handle the DataForm, Body or JsonBody.
56 | // Set right Content-Type.
57 | if req.PostForm != nil {
58 | httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
59 | data := req.PostForm.URLEncode()
60 | httpReq.Body = ioutil.NopCloser(strings.NewReader(data))
61 | } else if req.Body != nil {
62 | httpReq.Body = ioutil.NopCloser(bytes.NewReader(req.Body))
63 | } else if req.JsonBody != nil {
64 | httpReq.Header.Set("Content-Type", "application/json")
65 | httpReq.Body = ioutil.NopCloser(bytes.NewReader(req.JsonBody))
66 | } else if req.MultipartForm != nil {
67 | httpReq.Header.Set("Content-Type", req.MultipartForm.ContentType())
68 | httpReq.Body = ioutil.NopCloser(req.MultipartForm.Reader())
69 | }
70 |
71 | // Handle Cookies
72 | if req.Cookies != nil {
73 | for _, cookie := range req.Cookies {
74 | httpReq.AddCookie(cookie)
75 | }
76 | }
77 |
78 | resp, err := session.client.Do(httpReq) // do request
79 | if err != nil {
80 | if strings.Contains(err.Error(), "context deadline exceeded") { // check timeout error
81 | timeoutCancel()
82 | return nil, WrapErr(ErrTimeout, err.Error())
83 | }
84 | timeoutCancel()
85 | return nil, WrapErr(err, "Request Error")
86 | }
87 | defer func() {
88 | if err := resp.Body.Close(); err != nil {
89 | panic(err)
90 | }
91 | }()
92 |
93 | response, err := buildResponse(req, resp)
94 | if err != nil {
95 | timeoutCancel()
96 | return nil, WrapErr(err, "build Response Error")
97 | }
98 |
99 | timeoutCancel() // cancel the timeout context after request succeed.
100 | return response, nil
101 | }
102 |
103 | // buildResponse build response with http.Response after do request.
104 | func buildResponse(httpReq *Request, httpResp *http.Response) (*Response, error) {
105 | content, err := ioutil.ReadAll(httpResp.Body)
106 | if err != nil {
107 | if !errors.Is(err, io.ErrUnexpectedEOF) { // Ignore Unexpected EOF error
108 | return nil, WrapErr(err, "read Response.Body failed")
109 | }
110 | }
111 | return &Response{
112 | URL: httpReq.URL,
113 | StatusCode: httpResp.StatusCode,
114 | Proto: httpResp.Proto,
115 | Headers: httpResp.Header,
116 | Cookies: httpResp.Cookies(),
117 | Request: httpReq,
118 | ContentLength: httpResp.ContentLength,
119 | Content: content,
120 | encoding: "UTF-8",
121 | }, nil
122 | }
123 |
124 | // mergeHeaders merge Request headers and Session Headers.
125 | // Request has higher priority.
126 | func mergeHeaders(h1, h2 http.Header) http.Header {
127 | h := http.Header{}
128 | for key, values := range h2 {
129 | for _, value := range values {
130 | h.Set(key, value)
131 | }
132 | }
133 | for key, values := range h1 {
134 | for _, value := range values {
135 | h.Set(key, value)
136 | }
137 | }
138 | return h
139 | }
140 |
--------------------------------------------------------------------------------
/session_test.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func newTestSessionServer() *httptest.Server {
13 | gin.SetMode(gin.ReleaseMode)
14 | router := gin.New()
15 | router.GET("/test", func(c *gin.Context) {
16 | c.String(200, "GET")
17 | })
18 | router.GET("/setCookie", func(c *gin.Context) {
19 | http.SetCookie(c.Writer, &http.Cookie{
20 | Name: "key",
21 | Value: "value",
22 | Path: "/",
23 | Expires: time.Now().Add(30 * time.Second),
24 | })
25 | })
26 | router.GET("/getCookie", func(c *gin.Context) {
27 | cookies := c.Request.Cookies()
28 | for _, cookie := range cookies {
29 | c.String(200, cookie.Name+"="+cookie.Value)
30 | }
31 | })
32 | router.GET("/getHeader", func(c *gin.Context) {
33 | header := c.GetHeader("user-agent")
34 | c.String(200, header)
35 | })
36 | router.GET("/getParams", func(c *gin.Context) {
37 | value := c.Query("key")
38 | c.String(200, value)
39 | })
40 | router.GET("/proxy", func(c *gin.Context) {
41 | c.String(200, "This is target website.")
42 | })
43 | router.POST("/test", func(c *gin.Context) {
44 | data, err := c.GetRawData()
45 | if err != nil {
46 | c.AbortWithStatus(404)
47 | return
48 | }
49 | if string(data) == "key=value" {
50 | c.String(200, "POST")
51 | } else {
52 | c.String(200, "Failed")
53 | }
54 | })
55 | router.HEAD("/", func(c *gin.Context) {
56 | c.SetCookie("HEAD", "RIGHT", 1000, "/", "localhost", false, false)
57 | })
58 | router.PUT("/", func(c *gin.Context) {
59 | c.String(200, "PUT")
60 | })
61 | router.DELETE("/", func(c *gin.Context) {
62 | c.String(200, "DELETE")
63 | })
64 | router.PATCH("/", func(c *gin.Context) {
65 | c.String(200, "PATCH")
66 | })
67 |
68 | ts := httptest.NewServer(router)
69 | return ts
70 |
71 | //ys := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72 | // // check method is GET before going to check other features
73 | // if r.Method == "GET" {
74 | // if r.URL.Path == "/test" {
75 | // if _, err := w.Write([]byte("GET")); err != nil {
76 | // }
77 | // }
78 | // if r.URL.Path == "/setCookie" {
79 | // http.SetCookie(w, &http.Cookie{Name: "key", Value: "value"})
80 | // }
81 | // if r.URL.Path == "/getCookie" {
82 | // cookies := r.Cookies()
83 | // for _, cookie := range cookies {
84 | // if _, err := w.Write([]byte(cookie.Name + "=" + cookie.Value)); err != nil {
85 | // }
86 | // }
87 | // }
88 | // if r.URL.Path == "/getHeader" {
89 | // header := r.Header
90 | // value := header.Get("user-agent")
91 | // if _, err := w.Write([]byte(value)); err != nil {
92 | // }
93 | // }
94 | // if r.URL.Path == "/getParams" {
95 | // if err := r.ParseForm(); err != nil {
96 | // }
97 | // params := r.Form
98 | // value := params.Get("key")
99 | // if _, err := w.Write([]byte(value)); err != nil {
100 | // }
101 | // }
102 | // if r.URL.Path == "/proxy" {
103 | // if _, err := w.Write([]byte("This is target website.")); err != nil {
104 | // }
105 | // }
106 | // }
107 | // if r.Method == "POST" {
108 | // if r.URL.Path == "/test" {
109 | // body, _ := ioutil.ReadAll(r.Body)
110 | // if string(body) != "key=value" {
111 | // if _, err := w.Write([]byte("Failed")); err != nil {
112 | // }
113 | // } else {
114 | // if _, err := w.Write([]byte("POST")); err != nil {
115 | // }
116 | // }
117 | // }
118 | // }
119 | // if r.Method == "HEAD" {
120 | // http.SetCookie(w, &http.Cookie{Name: "HEAD", Value: "RIGHT"})
121 | // }
122 | // if r.Method == "PUT" {
123 | // if _, err := w.Write([]byte("PUT")); err != nil {
124 | // }
125 | // }
126 | // if r.Method == "PATCH" {
127 | // if _, err := w.Write([]byte("PATCH")); err != nil {
128 | // }
129 | // }
130 | // if r.Method == "DELETE" {
131 | // if _, err := w.Write([]byte("DELETE")); err != nil {
132 | // }
133 | // }
134 | //}))
135 | //return ys
136 | }
137 |
138 | func TestSessionGet(t *testing.T) {
139 | ts := newTestSessionServer()
140 | defer ts.Close()
141 |
142 | session := NewSession()
143 | resp, err := session.Get(ts.URL + "/test")
144 | if err != nil {
145 | t.Fatal(err)
146 | }
147 | text := resp.Text()
148 | if text != "GET" {
149 | t.Fatal("Session.Get test failed")
150 | }
151 | }
152 |
153 | func TestSessionPost(t *testing.T) {
154 | ts := newTestSessionServer()
155 | defer ts.Close()
156 |
157 | session := NewSession()
158 | postForm := NewPostForm(
159 | "key", "value",
160 | )
161 | resp, err := session.Post(ts.URL+"/test", postForm)
162 | if err != nil {
163 | t.Fatal(err)
164 | }
165 | text := resp.Text()
166 | if text != "POST" {
167 | t.Fatal("Session.Post test failed")
168 | }
169 |
170 | body := Body("key=value")
171 | resp2, err := session.Post(ts.URL+"/test", body)
172 | if err != nil {
173 | t.Fatal(err)
174 | }
175 | text2 := resp2.Text()
176 | if text2 != "POST" {
177 | t.Fatal("Session.Post test failed")
178 | }
179 | }
180 |
181 | func TestSessionPut(t *testing.T) {
182 | ts := newTestSessionServer()
183 | defer ts.Close()
184 |
185 | session := NewSession()
186 | resp, err := session.Put(ts.URL)
187 | if err != nil {
188 | t.Fatal(err)
189 | }
190 | text := resp.Text()
191 | if text != "PUT" {
192 | t.Fatal("Session.Put test failed")
193 | }
194 | }
195 |
196 | func TestSessionPatch(t *testing.T) {
197 | ts := newTestSessionServer()
198 | defer ts.Close()
199 |
200 | session := NewSession()
201 | resp, err := session.Patch(ts.URL)
202 | if err != nil {
203 | t.Fatal(err)
204 | }
205 | text := resp.Text()
206 | if text != "PATCH" {
207 | t.Fatal("Session.Patch test failed")
208 | }
209 | }
210 |
211 | func TestSessionDelete(t *testing.T) {
212 | ts := newTestSessionServer()
213 | defer ts.Close()
214 |
215 | session := NewSession()
216 | resp, err := session.Delete(ts.URL)
217 | if err != nil {
218 | t.Fatal(err)
219 | }
220 | text := resp.Text()
221 | if text != "DELETE" {
222 | t.Fatal("Session.Delete test failed")
223 | }
224 | }
225 |
226 | func TestSessionHead(t *testing.T) {
227 | ts := newTestSessionServer()
228 | defer ts.Close()
229 |
230 | session := NewSession()
231 | resp, err := session.Head(ts.URL)
232 | if err != nil {
233 | t.Fatal(err)
234 | }
235 | cookies := resp.Cookies
236 | if cookies[0].Name != "HEAD" {
237 | t.Fatal("Session.Head test failed")
238 | }
239 | }
240 |
241 | func TestSessionCookieJar(t *testing.T) {
242 | ts := newTestSessionServer()
243 | defer ts.Close()
244 |
245 | session := NewSession()
246 | _, err := session.Get(ts.URL + "/setCookie")
247 | if err != nil {
248 | t.Fatal(err)
249 | }
250 | resp, err := session.Get(ts.URL + "/getCookie")
251 | if err != nil {
252 | t.Fatal(err)
253 | }
254 | if resp.Text() != "key=value" {
255 | t.Fatal("Session.CookieJar failed.")
256 | return
257 | }
258 | }
259 |
260 | func TestSessionSetCookie(t *testing.T) {
261 | ts := newTestSessionServer()
262 | defer ts.Close()
263 |
264 | session := NewSession()
265 | cookie := NewCookies("key", "value")
266 | session.SetCookies(ts.URL, cookie)
267 | resp, err := session.Get(ts.URL + "/getCookie")
268 | if err != nil {
269 | t.Fatal(err)
270 | }
271 | if resp.Text() != "key=value" {
272 | t.Fatal("Session.SetCookies() failed.")
273 | return
274 | }
275 | }
276 |
277 | func TestSessionCookies(t *testing.T) {
278 | ts := newTestSessionServer()
279 | defer ts.Close()
280 |
281 | session := NewSession()
282 | _, err := session.Get(ts.URL + "/setCookie")
283 | if err != nil {
284 | t.Fatal(err)
285 | }
286 | cookies := session.Cookies(ts.URL)
287 | if cookies[0].Name != "key" {
288 | t.Fatal("Session.Cookies() failed.")
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/response.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net/http"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/PuerkitoBio/goquery"
9 | jsoniter "github.com/json-iterator/go"
10 | "github.com/tidwall/gjson"
11 | "golang.org/x/text/encoding/charmap"
12 | "golang.org/x/text/encoding/simplifiedchinese"
13 | )
14 |
15 | // Response is the response from request.
16 | type Response struct {
17 | URL string
18 | StatusCode int
19 | Proto string
20 | Headers http.Header
21 | Cookies Cookies
22 | Request *Request
23 | Content []byte
24 | ContentLength int64
25 | encoding string
26 | text string
27 | dom *goquery.Document
28 | }
29 |
30 | // Encoding can change and return the encoding type of response. Like this:
31 | // encoding := resp.Encoding("GBK")
32 | // You can specified encoding type. Such as GBK, GB18030, latin1. Default is UTF-8.
33 | // It will decode the content to string if you specified encoding type.
34 | // It will just return the encoding type of response if you do not pass parameter.
35 | func (resp *Response) Encoding(encoding ...string) string {
36 | if len(encoding) > 0 {
37 | resp.encoding = strings.ToUpper(encoding[0])
38 | resp.text = decodeContent(resp.encoding, resp.Content)
39 | }
40 | return resp.encoding
41 | }
42 |
43 | // Text return the text of Response. It will decode the content to string the first time
44 | // it is called.
45 | func (resp *Response) Text() string {
46 | if resp.text == "" {
47 | resp.text = decodeContent(resp.encoding, resp.Content)
48 | }
49 | return resp.text
50 | }
51 |
52 | // Re extract required data with regexp.
53 | // It return a slice of string.
54 | // Every time you call this method, it will transcode the Response.content to text once.
55 | // So please try to extract required data at once.
56 | func (resp *Response) Re(queryStr string) []string {
57 | text := resp.Text()
58 | return regexp.MustCompile(queryStr).FindAllString(text, -1)
59 | }
60 |
61 | // ReSubMatch extract required data with regexp.
62 | // It return a slice of string from FindAllStringSubmatch.
63 | // Every time you call this method, it will transcode the Response.content to text once.
64 | // So please try to extract required data at once.
65 | func (resp *Response) ReSubMatch(queryStr string) [][]string {
66 | text := resp.Text()
67 | data := regexp.MustCompile(queryStr).FindAllStringSubmatch(text, -1)
68 | var subMatchResult [][]string
69 | for _, match := range data {
70 | if len(match) > 1 { // In case that query has no sub match part
71 | subMatchResult = append(subMatchResult, match[1:])
72 | }
73 | }
74 | return subMatchResult
75 | }
76 |
77 | // CSS is a method to extract data with css selector, it returns a CSSNodeList.
78 | func (resp *Response) CSS(queryStr string) *CSSNodeList {
79 | if resp.dom == nil { // New the dom if resp.dom not exists.
80 | text := strings.NewReader(resp.Text())
81 | dom, err := goquery.NewDocumentFromReader(text)
82 | if err != nil {
83 | return nil
84 | }
85 | resp.dom = dom
86 | }
87 |
88 | newNodeList := make([]CSSNode, 0)
89 | resp.dom.Find(queryStr).Each(func(i int, selection *goquery.Selection) {
90 | newNode := CSSNode{selection: selection}
91 | newNodeList = append(newNodeList, newNode)
92 | })
93 | return &CSSNodeList{container: newNodeList}
94 | }
95 |
96 | // Json can unmarshal json type response body to a struct.
97 | func (resp *Response) Json(output interface{}) error {
98 | if err := jsoniter.Unmarshal(resp.Content, output); err != nil {
99 | return err
100 | }
101 | return nil
102 | }
103 |
104 | // JsonGet can get a value from json type response body with path.
105 | func (resp *Response) JsonGet(path string) gjson.Result {
106 | return gjson.GetBytes(resp.Content, path)
107 | }
108 |
109 | // decodeContent decode the content with the encodingType. It just support
110 | // UTF-8, GBK, GB18030, Lantin1 now.
111 | func decodeContent(encodingType string, content []byte) (decodedText string) {
112 | var text = ""
113 | switch encodingType {
114 | case "UTF-8", "UTF8":
115 | text = string(content)
116 | case "GBK":
117 | decodeBytes, err := simplifiedchinese.GBK.NewDecoder().Bytes(content)
118 | if err != nil {
119 | return ""
120 | }
121 | text = string(decodeBytes)
122 | case "GB18030":
123 | decodeBytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(content)
124 | if err != nil {
125 | return ""
126 | }
127 | text = string(decodeBytes)
128 | case "LATIN1":
129 | decodeBytes, err := charmap.ISO8859_1.NewDecoder().Bytes(content)
130 | if err != nil {
131 | return ""
132 | }
133 | text = string(decodeBytes)
134 | }
135 | return text
136 | }
137 |
138 | // CSSNode is a container that stores single selected results
139 | type CSSNode struct {
140 | selection *goquery.Selection
141 | }
142 |
143 | // Text return the text of the CSSNode. Only include straight children node text
144 | func (node *CSSNode) Text() string {
145 | if node.selection != nil {
146 | var text string
147 | node.selection.Contents().Each(func(i int, s *goquery.Selection) {
148 | if goquery.NodeName(s) == "#text" {
149 | t := s.Text()
150 | text = text + t
151 | }
152 | })
153 | return text
154 | }
155 | return ""
156 | }
157 |
158 | // TextAll return the text of the CSSNode. Include all children node text
159 | func (node *CSSNode) TextAll() string {
160 | if node.selection != nil {
161 | return node.selection.Text()
162 | }
163 | return ""
164 | }
165 |
166 | // Attr return the attribute value of the CSSNode.
167 | // You can set default value, if value isn`t exists, return default value.
168 | func (node *CSSNode) Attr(attrName string, defaultValue ...string) string {
169 | var d, attrValue string
170 | if node.selection != nil {
171 | if len(defaultValue) > 0 {
172 | d = defaultValue[0]
173 | }
174 | attrValue = node.selection.AttrOr(attrName, d)
175 | }
176 | return attrValue
177 | }
178 |
179 | // CSSNodeList is a container that stores selected results
180 | type CSSNodeList struct {
181 | container []CSSNode
182 | }
183 |
184 | // Text return a list of text. Only include straight children node text
185 | func (nodeList *CSSNodeList) Text() (textList []string) {
186 | for _, node := range nodeList.container {
187 | text := node.Text()
188 | if text != "" {
189 | textList = append(textList, text)
190 | }
191 | }
192 | return
193 | }
194 |
195 | // TextAll return a list of text. Include all children node text
196 | func (nodeList *CSSNodeList) TextAll() (textList []string) {
197 | for _, node := range nodeList.container {
198 | text := node.TextAll()
199 | if text != "" {
200 | textList = append(textList, text)
201 | }
202 | }
203 | return
204 | }
205 |
206 | // Attr return a list of attribute value
207 | func (nodeList *CSSNodeList) Attr(attrName string, defaultValue ...string) (valueList []string) {
208 | for _, node := range nodeList.container {
209 | value := node.Attr(attrName, defaultValue...)
210 | if value != "" {
211 | valueList = append(valueList, value)
212 | }
213 | }
214 | return
215 | }
216 |
217 | // CSS return a CSSNodeList, so you can chain CSS
218 | func (nodeList *CSSNodeList) CSS(queryStr string) *CSSNodeList {
219 | newNodeList := make([]CSSNode, 0)
220 | for _, node := range nodeList.container {
221 | node.selection.Find(queryStr).Each(func(i int, selection *goquery.Selection) {
222 | newNode := CSSNode{selection: selection}
223 | newNodeList = append(newNodeList, newNode)
224 | })
225 | }
226 | return &CSSNodeList{container: newNodeList}
227 | }
228 |
229 | // First return the first cssNode of CSSNodeList.
230 | // Return a empty cssNode if there is no cssNode in CSSNodeList
231 | func (nodeList *CSSNodeList) First() *CSSNode {
232 | if len(nodeList.container) > 0 {
233 | return &nodeList.container[0]
234 | }
235 | return &CSSNode{}
236 | }
237 |
238 | // At return the cssNode of specified index position.
239 | // Return a empty cssNode if there is no cssNode in CSSNodeList
240 | func (nodeList *CSSNodeList) At(index int) *CSSNode {
241 | if len(nodeList.container) > index {
242 | return &nodeList.container[index]
243 | }
244 | return &CSSNode{}
245 | }
246 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
2 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
3 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
4 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
5 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
6 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
7 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
12 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
13 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
14 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
15 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
16 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
17 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
18 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
19 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
20 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
21 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
22 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
23 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
24 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
25 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
26 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
27 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
28 | github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
29 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
30 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
31 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
32 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
33 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
35 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
36 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
41 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
42 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
43 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
44 | github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
45 | github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
46 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
47 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
48 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
49 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
50 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
51 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
52 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
53 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
54 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
55 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
56 | github.com/valyala/fasthttp v1.35.0 h1:wwkR8mZn2NbigFsaw2Zj5r+xkmzjbrA/lyTmiSlal/Y=
57 | github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
58 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
60 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
61 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
62 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
63 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
64 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
65 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
66 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
67 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
68 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
70 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
73 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
74 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
77 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
79 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
81 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
82 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
83 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
88 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
89 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
90 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
91 |
--------------------------------------------------------------------------------
/datatype.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "sort"
7 | "strings"
8 |
9 | jsoniter "github.com/json-iterator/go"
10 | )
11 |
12 | // RequestOption is the interface of Request Options. Use to bind the options to Request.
13 | type RequestOption interface {
14 | // Bind Options to Request
15 | bindRequest(request *Request) error
16 | }
17 |
18 | // Body is the data you want to post, one of the Request Options.
19 | type Body []byte
20 |
21 | // RequestOption interface method, bind request option to request.
22 | func (options Body) bindRequest(request *Request) error {
23 | request.Body = options
24 | return nil
25 | }
26 |
27 | // JsonBody is the json data you want to post.
28 | type JsonBody []byte
29 |
30 | // NewJsonBody new a json type body.
31 | func NewJsonBody(v interface{}) JsonBody {
32 | body, err := jsoniter.Marshal(v)
33 | if err != nil {
34 | return nil
35 | }
36 | return body
37 | }
38 |
39 | // RequestOption interface method, bind request option to request.
40 | func (options JsonBody) bindRequest(request *Request) error {
41 | request.Body = options
42 | return nil
43 | }
44 |
45 | // RedirectNum is the number of request redirect allowed.
46 | // If RedirectNum > 0, it means a redirect number limit for requests.
47 | // If RedirectNum <= 0, it means ban redirect.
48 | // If RedirectNum is not set, it means default 5 times redirect limit.
49 | type RedirectNum int
50 |
51 | // RequestOption interface method, bind request option to request.
52 | func (options RedirectNum) bindRequest(request *Request) error {
53 | request.RedirectNum = int(options)
54 | return nil
55 | }
56 |
57 | // Timeout is the number of time to timeout request.
58 | // if timeout > 0, it means a time limit for requests.
59 | // if timeout < 0, it means no limit.
60 | // if timeout = 0, it means keep default 30 second timeout.
61 | type Timeout int
62 |
63 | // RequestOption interface method, bind request option to request.
64 | func (options Timeout) bindRequest(request *Request) error {
65 | request.Timeout = int(options)
66 | return nil
67 | }
68 |
69 | // Proxy is the proxy server address, like "http://127.0.0.1:1080".
70 | // You can set different proxies for HTTP and HTTPS sites.
71 | type Proxy struct {
72 | HTTP string
73 | HTTPS string
74 | }
75 |
76 | // RequestOption interface method, bind request option to request.
77 | func (options *Proxy) bindRequest(request *Request) error {
78 | request.Proxy = options
79 | return nil
80 | }
81 |
82 | // strSliceMap type is map[string][]string, used for Params, PostForm.
83 | type strSliceMap struct {
84 | data map[string][]string
85 | }
86 |
87 | // New is the way to create a strSliceMap.
88 | // You can set key-value pair when you init it by sent params. Just like this:
89 | // stringSliceMap{}.New(
90 | // "key1", "value1",
91 | // "key2", "value2",
92 | // )
93 | // But be careful, between the key and value is a comma.
94 | // And if the number of parameters is not a multiple of 2, it will panic.
95 | func (ssm *strSliceMap) New(keyValue ...string) {
96 | ssm.data = make(map[string][]string)
97 | if keyValue != nil {
98 | if len(keyValue)%2 != 0 {
99 | panic("key and value must be pair")
100 | }
101 |
102 | for i := 0; i < len(keyValue)/2; i++ {
103 | key := keyValue[i*2]
104 | value := keyValue[i*2+1]
105 | ssm.data[key] = append(ssm.data[key], value)
106 | }
107 | }
108 | }
109 |
110 | // Add key and value to stringSliceMap.
111 | // If key exists, value will append to slice.
112 | func (ssm *strSliceMap) Add(key, value string) {
113 | ssm.data[key] = append(ssm.data[key], value)
114 | }
115 |
116 | // Set key and value to stringSliceMap.
117 | // If key exists, existed value will drop and new value will set.
118 | func (ssm *strSliceMap) Set(key, value string) {
119 | ssm.data[key] = []string{value}
120 | }
121 |
122 | // Del delete the given key.
123 | func (ssm *strSliceMap) Del(key string) {
124 | delete(ssm.data, key)
125 | }
126 |
127 | // Get get the value pair to given key.
128 | // You can pass index to assign which value to get, when there are multiple values.
129 | func (ssm *strSliceMap) Get(key string, index ...int) string {
130 | if ssm.data == nil {
131 | return ""
132 | }
133 | ssmValue := ssm.data[key]
134 | if len(ssmValue) == 0 {
135 | return ""
136 | }
137 | if index != nil {
138 | return ssmValue[index[0]]
139 | }
140 | return ssmValue[0]
141 | }
142 |
143 | // URLEncode encodes the values into ``URL encoded'' form
144 | // ("bar=baz&foo=qux") sorted by key.
145 | func (ssm *strSliceMap) URLEncode() string {
146 | if ssm.data == nil {
147 | return ""
148 | }
149 | var buf strings.Builder
150 | keys := make([]string, 0, len(ssm.data))
151 | for k := range ssm.data {
152 | keys = append(keys, k)
153 | }
154 | sort.Strings(keys)
155 | for _, k := range keys {
156 | ssmValue := ssm.data[k]
157 | keyEscaped := url.QueryEscape(k)
158 | for _, v := range ssmValue {
159 | if buf.Len() > 0 {
160 | buf.WriteByte('&')
161 | }
162 | buf.WriteString(keyEscaped)
163 | buf.WriteByte('=')
164 | buf.WriteString(url.QueryEscape(v))
165 | }
166 | }
167 | return buf.String()
168 | }
169 |
170 | // Params is url params you want to join to url, as parameter in Request method.
171 | // You should init it by using NewParams like this:
172 | // params := dw.NewParams(
173 | // "key1", "value1",
174 | // "key2", "value2",
175 | // )
176 | // Note: mid symbol is comma.
177 | type Params struct {
178 | strSliceMap
179 | }
180 |
181 | // NewParams new a Params type.
182 | //
183 | // You can set key-value pair when you init it by sent parameters. Just like this:
184 | // params := NewParams(
185 | // "key1", "value1",
186 | // "key2", "value2",
187 | // )
188 | // But be careful, between the key and value is a comma.
189 | // And if the number of parameters is not a multiple of 2, it will panic.
190 | func NewParams(keyValue ...string) *Params {
191 | var p = &Params{}
192 | p.New(keyValue...)
193 | return p
194 | }
195 |
196 | // RequestOption interface method, bind request option to request.
197 | func (options *Params) bindRequest(request *Request) error {
198 | request.Params = options
199 | u, err := url.Parse(request.URL)
200 | if err != nil {
201 | return WrapErrf(err, "URL error")
202 | }
203 |
204 | // check whether parameters is existed in url.
205 | if u.RawQuery == "" && u.ForceQuery == false {
206 | request.URL = request.URL + "?" + request.Params.URLEncode()
207 | } else if u.RawQuery == "" && u.ForceQuery == true {
208 | request.URL = request.URL + request.Params.URLEncode()
209 | } else {
210 | request.URL = request.URL + "&" + request.Params.URLEncode()
211 | }
212 | return nil
213 | }
214 |
215 | // PostForm is the form you want to post, as parameter in Request method.
216 | // You should init it by using NewPostForm like this:
217 | // postForm := dw.NewPostForm(
218 | // "key1", "value1",
219 | // "key2", "value2",
220 | // )
221 | // Note: mid symbol is comma.
222 | type PostForm struct {
223 | strSliceMap
224 | }
225 |
226 | // NewPostForm new a PostForm type.
227 | //
228 | // You can set key-value pair when you init it by sent parameters. Just like this:
229 | // postForm := NewPostForm(
230 | // "key1", "value1",
231 | // "key2", "value2",
232 | // )
233 | // But be careful, between the key and value is a comma.
234 | // And if the number of parameters is not a multiple of 2, it will panic.
235 | func NewPostForm(keyValue ...string) *PostForm {
236 | var p = &PostForm{}
237 | p.New(keyValue...)
238 | return p
239 | }
240 |
241 | // RequestOption interface method, bind request option to request.
242 | func (options *PostForm) bindRequest(request *Request) error {
243 | request.PostForm = options
244 | return nil
245 | }
246 |
247 | type Headers struct {
248 | http.Header
249 | }
250 |
251 | // RequestOption interface method, bind request option to request.
252 | func (options Headers) bindRequest(request *Request) error {
253 | request.Headers = options.Header
254 | return nil
255 | }
256 |
257 | // NewHeaders new a http.Header type.
258 | //
259 | // You can set key-value pair when you init it by sent parameters. Just like this:
260 | // headers := NewHeaders(
261 | // "key1", "value1",
262 | // "key2", "value2",
263 | // )
264 | // But be careful, between the key and value is a comma.
265 | // And if the number of parameters is not a multiple of 2, it will panic.
266 | func NewHeaders(keyValue ...string) *Headers {
267 | h := new(Headers)
268 | h.Header = http.Header{}
269 | if keyValue != nil {
270 | if len(keyValue)%2 != 0 {
271 | panic("key and value must be part")
272 | }
273 |
274 | for i := 0; i < len(keyValue)/2; i++ {
275 | key := keyValue[i*2]
276 | value := keyValue[i*2+1]
277 | h.Add(key, value)
278 | }
279 | }
280 | return h
281 | }
282 |
283 | // Cookies is request cookies, as parameter in Request method.
284 | // You should init it by using NewCookies like this:
285 | // cookies := dw.NewCookies(
286 | // "key1", "value1",
287 | // "key2", "value2",
288 | // )
289 | // Note: mid symbol is comma.
290 | type Cookies []*http.Cookie
291 |
292 | // NewCookies new a Cookies type.
293 | //
294 | // You can set key-value pair when you init it by sent parameters. Just like this:
295 | // cookies := NewCookies(
296 | // "key1", "value1",
297 | // "key2", "value2",
298 | // )
299 | // But be careful, between the key and value is a comma.
300 | // And if the number of parameters is not a multiple of 2, it will panic.
301 | func NewCookies(keyValue ...string) Cookies {
302 | c := make(Cookies, 0)
303 | if keyValue != nil {
304 | if len(keyValue)%2 != 0 {
305 | panic("key and value must be part")
306 | }
307 |
308 | for i := 0; i < len(keyValue)/2; i++ {
309 | key := keyValue[i*2]
310 | value := keyValue[i*2+1]
311 | cookie := &http.Cookie{Name: key, Value: value}
312 | c = append(c, cookie)
313 | }
314 | }
315 | return c
316 | }
317 |
318 | // RequestOption interface method, bind request option to request.
319 | func (c Cookies) bindRequest(request *Request) error {
320 | request.Cookies = c
321 | return nil
322 | }
323 |
324 | // Add append a new cookie to Cookies.
325 | func (c Cookies) Add(key, value string) {
326 | c = append(c, &http.Cookie{Name: key, Value: value})
327 | }
328 |
--------------------------------------------------------------------------------
/session.go:
--------------------------------------------------------------------------------
1 | package direwolf
2 |
3 | import (
4 | "net"
5 | "net/http"
6 | "net/http/cookiejar"
7 | "net/url"
8 | "sync"
9 | "time"
10 |
11 | "golang.org/x/net/http/httpproxy"
12 |
13 | "golang.org/x/net/publicsuffix"
14 | )
15 |
16 | // Session is the main object in direwolf. This is its main features:
17 | // 1. handling redirects
18 | // 2. automatically managing cookies
19 | type Session struct {
20 | client *http.Client
21 | transport *http.Transport
22 | Headers http.Header
23 | Proxy *Proxy
24 | Timeout int
25 | }
26 |
27 | // NewSession new a Session object, and set a default Client and Transport.
28 | func NewSession(options ...*SessionOptions) *Session {
29 | var sessionOptions *SessionOptions
30 | if len(options) > 0 {
31 | sessionOptions = options[0]
32 | } else {
33 | sessionOptions = DefaultSessionOptions()
34 | }
35 |
36 | // set transport parameters.
37 | trans := &http.Transport{
38 | DialContext: (&net.Dialer{
39 | Timeout: sessionOptions.DialTimeout,
40 | KeepAlive: sessionOptions.DialKeepAlive,
41 | }).DialContext,
42 | MaxIdleConns: sessionOptions.MaxIdleConns,
43 | MaxIdleConnsPerHost: sessionOptions.MaxIdleConnsPerHost,
44 | MaxConnsPerHost: sessionOptions.MaxConnsPerHost,
45 | IdleConnTimeout: sessionOptions.IdleConnTimeout,
46 | TLSHandshakeTimeout: sessionOptions.TLSHandshakeTimeout,
47 | ExpectContinueTimeout: sessionOptions.ExpectContinueTimeout,
48 | Proxy: proxyFunc,
49 | }
50 | if sessionOptions.DisableDialKeepAlives {
51 | trans.DisableKeepAlives = true
52 | }
53 |
54 | client := &http.Client{
55 | Transport: trans,
56 | CheckRedirect: redirectFunc,
57 | }
58 |
59 | // set CookieJar
60 | if sessionOptions.DisableCookieJar == false {
61 | cookieJarOptions := cookiejar.Options{
62 | PublicSuffixList: publicsuffix.List,
63 | }
64 | jar, err := cookiejar.New(&cookieJarOptions)
65 | if err != nil {
66 | return nil
67 | }
68 | client.Jar = jar
69 | }
70 |
71 | // Set default user agent
72 | headers := http.Header{}
73 | headers.Add("User-Agent", "direwolf - winter is coming")
74 |
75 | return &Session{
76 | client: client,
77 | transport: trans,
78 | Headers: headers,
79 | }
80 | }
81 |
82 | // Send is a generic request method.
83 | func (session *Session) Send(req *Request) (*Response, error) {
84 | resp, err := send(session, req)
85 | if err != nil {
86 | return nil, WrapErr(err, "session send failed")
87 | }
88 | return resp, nil
89 | }
90 |
91 | // Get is a get method.
92 | func (session *Session) Get(URL string, args ...RequestOption) (*Response, error) {
93 | req, err := NewRequest("GET", URL, args...)
94 | if err != nil {
95 | return nil, err
96 | }
97 | resp, err := session.Send(req)
98 | if err != nil {
99 | return nil, err
100 | }
101 | return resp, nil
102 | }
103 |
104 | // Post is a post method.
105 | func (session *Session) Post(URL string, args ...RequestOption) (*Response, error) {
106 | req, err := NewRequest("POST", URL, args...)
107 | if err != nil {
108 | return nil, err
109 | }
110 | resp, err := session.Send(req)
111 | if err != nil {
112 | return nil, err
113 | }
114 | return resp, nil
115 | }
116 |
117 | // Head is a post method.
118 | func (session *Session) Head(URL string, args ...RequestOption) (*Response, error) {
119 | req, err := NewRequest("HEAD", URL, args...)
120 | if err != nil {
121 | return nil, err
122 | }
123 | resp, err := session.Send(req)
124 | if err != nil {
125 | return nil, err
126 | }
127 | return resp, nil
128 | }
129 |
130 | // Put is a post method.
131 | func (session *Session) Put(URL string, args ...RequestOption) (*Response, error) {
132 | req, err := NewRequest("PUT", URL, args...)
133 | if err != nil {
134 | return nil, err
135 | }
136 | resp, err := session.Send(req)
137 | if err != nil {
138 | return nil, err
139 | }
140 | return resp, nil
141 | }
142 |
143 | // Patch is a post method.
144 | func (session *Session) Patch(URL string, args ...RequestOption) (*Response, error) {
145 | req, err := NewRequest("PATCH", URL, args...)
146 | if err != nil {
147 | return nil, err
148 | }
149 | resp, err := session.Send(req)
150 | if err != nil {
151 | return nil, err
152 | }
153 | return resp, nil
154 | }
155 |
156 | // Delete is a post method.
157 | func (session *Session) Delete(URL string, args ...RequestOption) (*Response, error) {
158 | req, err := NewRequest("DELETE", URL, args...)
159 | if err != nil {
160 | return nil, err
161 | }
162 | resp, err := session.Send(req)
163 | if err != nil {
164 | return nil, err
165 | }
166 | return resp, nil
167 | }
168 |
169 | // Cookies returns the cookies of the given url in Session.
170 | func (session *Session) Cookies(URL string) Cookies {
171 | if session.client.Jar == nil {
172 | return nil
173 | }
174 | parsedURL, err := url.Parse(URL)
175 | if err != nil {
176 | return nil
177 | }
178 | return session.client.Jar.Cookies(parsedURL)
179 | }
180 |
181 | // SetCookies set cookies of the url in Session.
182 | func (session *Session) SetCookies(URL string, cookies Cookies) {
183 | if session.client.Jar == nil {
184 | return
185 | }
186 | parsedURL, err := url.Parse(URL)
187 | if err != nil {
188 | return
189 | }
190 | session.client.Jar.SetCookies(parsedURL, cookies)
191 | }
192 |
193 | type SessionOptions struct {
194 | // DialTimeout is the maximum amount of time a dial will wait for
195 | // a connect to complete.
196 | //
197 | // When using TCP and dialing a host name with multiple IP
198 | // addresses, the timeout may be divided between them.
199 | //
200 | // With or without a timeout, the operating system may impose
201 | // its own earlier timeout. For instance, TCP timeouts are
202 | // often around 3 minutes.
203 | DialTimeout time.Duration
204 |
205 | // DialKeepAlive specifies the interval between keep-alive
206 | // probes for an active network connection.
207 | //
208 | // Network protocols or operating systems that do
209 | // not support keep-alives ignore this field.
210 | // If negative, keep-alive probes are disabled.
211 | DialKeepAlive time.Duration
212 |
213 | // MaxConnsPerHost optionally limits the total number of
214 | // connections per host, including connections in the dialing,
215 | // active, and idle states. On limit violation, dials will block.
216 | //
217 | // Zero means no limit.
218 | MaxConnsPerHost int
219 |
220 | // MaxIdleConns controls the maximum number of idle (keep-alive)
221 | // connections across all hosts. Zero means no limit.
222 | MaxIdleConns int
223 |
224 | // MaxIdleConnsPerHost, if non-zero, controls the maximum idle
225 | // (keep-alive) connections to keep per-host. If zero,
226 | // DefaultMaxIdleConnsPerHost is used.
227 | MaxIdleConnsPerHost int
228 |
229 | // IdleConnTimeout is the maximum amount of time an idle
230 | // (keep-alive) connection will remain idle before closing
231 | // itself.
232 | // Zero means no limit.
233 | IdleConnTimeout time.Duration
234 |
235 | // TLSHandshakeTimeout specifies the maximum amount of time waiting to
236 | // wait for a TLS handshake. Zero means no timeout.
237 | TLSHandshakeTimeout time.Duration
238 |
239 | // ExpectContinueTimeout, if non-zero, specifies the amount of
240 | // time to wait for a server's first response headers after fully
241 | // writing the request headers if the request has an
242 | // "Expect: 100-continue" header. Zero means no timeout and
243 | // causes the body to be sent immediately, without
244 | // waiting for the server to approve.
245 | // This time does not include the time to send the request header.
246 | ExpectContinueTimeout time.Duration
247 |
248 | // DisableCookieJar specifies whether disable session cookiejar.
249 | DisableCookieJar bool
250 |
251 | // DisableDialKeepAlives, if true, disables HTTP keep-alives and
252 | // will only use the connection to the server for a single
253 | // HTTP request.
254 | //
255 | // This is unrelated to the similarly named TCP keep-alives.
256 | DisableDialKeepAlives bool
257 | }
258 |
259 | // DefaultSessionOptions return a default SessionOptions object.
260 | func DefaultSessionOptions() *SessionOptions {
261 | return &SessionOptions{
262 | DialTimeout: 30 * time.Second,
263 | DialKeepAlive: 30 * time.Second,
264 | MaxConnsPerHost: 0,
265 | MaxIdleConns: 100,
266 | MaxIdleConnsPerHost: 2,
267 | IdleConnTimeout: 90 * time.Second,
268 | TLSHandshakeTimeout: 10 * time.Second,
269 | ExpectContinueTimeout: 1 * time.Second,
270 | DisableCookieJar: false,
271 | DisableDialKeepAlives: false,
272 | }
273 | }
274 |
275 | var (
276 | // proxyConfigOnce guards proxyConfig
277 | envProxyOnce sync.Once
278 | envProxyFuncValue func(*url.URL) (*url.URL, error)
279 | )
280 |
281 | // proxyFunc get proxy from request context.
282 | // If there is no proxy set, use default proxy from environment.
283 | func proxyFunc(req *http.Request) (*url.URL, error) {
284 | httpURLStr := req.Context().Value("http") // get http proxy url form context
285 | httpsURLStr := req.Context().Value("https") // get https proxy url form context
286 |
287 | // If there is no proxy set, use default proxy from environment.
288 | // This mitigates expensive lookups on some platforms (e.g. Windows).
289 | envProxyOnce.Do(func() {
290 | envProxyFuncValue = httpproxy.FromEnvironment().ProxyFunc()
291 | })
292 |
293 | if req.URL.Scheme == "http" { // set proxy for http site
294 | if httpURLStr != nil {
295 | httpURL, err := url.Parse(httpURLStr.(string))
296 | if err != nil {
297 | return nil, WrapErr(err, "HTTP Proxy error, please check proxy url")
298 | }
299 | return httpURL, nil
300 | }
301 | } else if req.URL.Scheme == "https" { // set proxy for https site
302 | if httpsURLStr != nil {
303 | httpsURL, err := url.Parse(httpsURLStr.(string))
304 | if err != nil {
305 | return nil, WrapErr(err, "HTTPS Proxy error, please check proxy url")
306 | }
307 | return httpsURL, nil
308 | }
309 | }
310 |
311 | return envProxyFuncValue(req.URL)
312 | }
313 |
314 | // redirectFunc get redirectNum from request context and check redirect number.
315 | func redirectFunc(req *http.Request, via []*http.Request) error {
316 | redirectNum := req.Context().Value("redirectNum").(int)
317 | if len(via) > redirectNum {
318 | err := &RedirectError{redirectNum}
319 | return WrapErr(err, "RedirectError")
320 | }
321 | return nil
322 | }
323 |
--------------------------------------------------------------------------------