diff --git a/.gitignore b/.gitignore index 9e856bd4..cd236c63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o +.vscode/ *.a *.so @@ -26,5 +27,8 @@ _testmain.go coverage.out coverage.txt +# Mac osx +.DS_Store + # Exclude intellij IDE folders .idea/* diff --git a/client.go b/client.go index 446ba851..728ce6a1 100644 --- a/client.go +++ b/client.go @@ -1375,6 +1375,7 @@ func createClient(hc *http.Client) *Client { parseRequestBody, createHTTPRequest, addCredentials, + createCurlCmd, } // user defined request middlewares diff --git a/example1/req_build_test.go b/example1/req_build_test.go new file mode 100644 index 00000000..e372965e --- /dev/null +++ b/example1/req_build_test.go @@ -0,0 +1,51 @@ +package examples + +import ( + ioutil "io" + "regexp" + "testing" + + "github.com/ahuigo/requests/v2" +) + +// TestBuildRequest +func TestBuildRequest(t *testing.T) { + req, err := requests.BuildRequest("post", "http://baidu.com/a/b/c", requests.Json{ + "age": 1, + }) + if err != nil { + t.Fatal(err) + } + body, _ := ioutil.ReadAll(req.Body) + expectedBody := `{"age":1}` + if string(body) != expectedBody { + t.Fatal("Failed to build request") + } +} +func TestBuildCurlRequest(t *testing.T) { + req, _ := requests.BuildRequest("post", "https://baidu.com/path?q=curl&v=1", requests.Json{ + "age": 1, + }) + curl := requests.BuildCurlRequest(req) + if !regexp.MustCompile(`^curl -X POST .+ 'https://baidu.com/path\?q=curl&v=1'`).MatchString(curl) { + t.Fatal(`bad curl cmd: ` + curl) + } + t.Log(curl) +} + +func TestBuildRequestHost(t *testing.T) { + req, err := requests.BuildRequest("post", "http://baidu.com/a/b/c", requests.Json{ + "age": 1, + }) + if err != nil { + t.Fatal(err) + } + if req.Host != "baidu.com" { + t.Fatalf("bad host:%s\n", req.Host) + } + + req, _ = requests.BuildRequest("post", "http://baidu.com/a/b/c", requests.Header{"Host": "ahuigo.com"}) + if req.Host != "ahuigo.com" { + t.Fatalf("bad host:%s\n", req.Host) + } +} diff --git a/example1/req_header_global_test.go b/example1/req_header_global_test.go new file mode 100644 index 00000000..ce3464f2 --- /dev/null +++ b/example1/req_header_global_test.go @@ -0,0 +1,22 @@ +package examples + +import ( + "testing" + + "github.com/ahuigo/requests/v2" +) + +// Set session headers +func TestSendGlobalHeader2(t *testing.T) { + session := requests.R() + + headerK := "User-Agent" + headerV := "Custom-Test-Go-User-Agent" + req, err := session.SetGlobalHeader(headerK, headerV).BuildRequest("post", "http://baidu.com/a/b/c") + if err != nil { + t.Fatal(err) + } + if req.Header.Get(headerK) != headerV { + t.Fatalf("Expected header %s is %s", headerK, headerV) + } +} diff --git a/example1/req_option_test.go b/example1/req_option_test.go new file mode 100644 index 00000000..92fd79e2 --- /dev/null +++ b/example1/req_option_test.go @@ -0,0 +1,26 @@ +package examples + +import ( + "testing" + + "github.com/ahuigo/requests/v2" +) + +// Test Session with cookie +func TestSessionWithCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + req := requests.R().SetDebug() + _, err := req.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + resp, err := req.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + if resp.GetCookie("count") != "2" { + t.Fatal("Failed to set cookie count") + } +} diff --git a/example1/ssl_test.go b/example1/ssl_test.go new file mode 100644 index 00000000..8a86dc1a --- /dev/null +++ b/example1/ssl_test.go @@ -0,0 +1,133 @@ +package examples + +import ( + "context" + "crypto/tls" + "crypto/x509" + "log" + "net" + "strings" + "testing" + + "github.com/ahuigo/requests/v2" +) + +func TestSkipSsl(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + session := requests.R() + + // 2. fake CA certificate + // session.SetCaCert("conf/rootCA.crt") + + // 3. skip ssl + session = session.SkipSsl(true) + + // 4. send get request + resp, err := session.Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if resp.Text() == "" { + t.Fatal(resp.Text()) + } +} + +func TestSslSkipViaTransport(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + session := requests.R() + + // 3. skip ssl & proxy connect + tsp := session.GetTransport() + _ = tsp + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + tsp.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + + // 4. send get request + resp, err := session.Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if resp.Text() == "" { + t.Fatal(resp.Text()) + } +} + +func TestSslCertSelf(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(1) + defer ts.Close() + + session := requests.R() + // 2. certs + certs := x509.NewCertPool() + for _, c := range ts.TLS.Certificates { + roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1]) + if err != nil { + log.Fatalf("error parsing server's root cert: %v", err) + } + for _, root := range roots { + certs.AddCert(root) + } + } + + // 3. 代替 session.SetCaCert("tmp/ca.crt") + // 3. with RootCAs & proxy connect + tsp := session.GetTransport() + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + tsp.TLSClientConfig = &tls.Config{ + // InsecureSkipVerify: true, + RootCAs: certs, + } + + // 4. send get request + resp, err := session.Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if resp.Text() == "" { + t.Fatal(resp.Text()) + } +} + +// go test -timeout 6000s -run '^TesSslCertCustom$' github.com/ahuigo/requests/v2/examples -v -httptest.serve=127.0.0.1:443 +func TesSslCertCustom(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + session := requests.R() + + // 2. fake CA or self-signed certificate like nginx.crt + session.SetCaCert("conf/nginx.crt") + tsp := session.GetTransport() + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + + url := strings.Replace(ts.URL, "127.0.0.1", "local.self", 1) + "/get?a=1" + t.Log(url) + // time.Sleep(10 * time.Minute) + // 4. send get request + resp, err := session.Get(url) + if err != nil { + t.Fatal(err) + } + if resp.Text() == "" { + t.Fatal(resp.Text()) + } +} diff --git a/example1/trace_test.go b/example1/trace_test.go new file mode 100644 index 00000000..eead3bba --- /dev/null +++ b/example1/trace_test.go @@ -0,0 +1,23 @@ +/** + * refer to: git@github.com:go-resty/resty.git + */ +package examples + +import ( + "testing" + + "github.com/ahuigo/requests/v2" +) + +// test context: cancel multi +func TestTrace(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + params := requests.Params{"name": "ahuigo", "page": "1"} + resp, err := requests.R().SetDebug().Get(ts.URL+"/get", params) + if err != nil { + t.Fatal(err) + } + t.Logf("connTime:%+v", resp.TraceInfo.ConnTime) +} diff --git a/example1/transport_test.go b/example1/transport_test.go new file mode 100644 index 00000000..ce206f1c --- /dev/null +++ b/example1/transport_test.go @@ -0,0 +1,32 @@ +/** + * refer to: git@github.com:go-resty/resty.git + */ +package examples + +import ( + "net/http" + "testing" + + "github.com/ahuigo/requests/v2" +) + +func TestTransportSet(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + session := requests.R() + // tsp:= otelhttp.NewTransport(http.DefaultTransport) + tsp := http.DefaultTransport.(*http.Transport).Clone() + tsp.MaxIdleConnsPerHost = 1 + tsp.MaxIdleConns = 1 + tsp.MaxConnsPerHost = 1 + session.SetTransport(tsp) + + resp, err := session.Get(ts.URL + "/sleep/11") + if err != nil { + t.Fatal(err) + } + if resp.Text() == "" { + t.Fatal(resp.Text()) + } +} diff --git a/examples/auth_test.go b/examples/auth_test.go new file mode 100644 index 00000000..75e6d3a6 --- /dev/null +++ b/examples/auth_test.go @@ -0,0 +1,25 @@ +package examples + +import ( + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + + +func TestAuth(t *testing.T) { + ts := createEchoServer() + defer ts.Close() + // test authentication usernae,password + client := resty.New() + resp, err := client.R().SetBasicAuth("httpwatch", "foo").Get( ts.URL+"/echo",) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(resp.Body()), "Authorization: Basic ") { + t.Fatal("bad auth body:\n" + resp.String()) + } + // this save file test PASS + // resp.SaveFile("auth.jpeg") +} diff --git a/examples/context_test.go b/examples/context_test.go new file mode 100644 index 00000000..98f92f4d --- /dev/null +++ b/examples/context_test.go @@ -0,0 +1,122 @@ +/** + * refer to: git@github.com:go-resty/resty.git + */ +package examples + +import ( + "context" + "net/http" + "net/http/httptrace" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +// test context: cancel multi +func TestSetContextCancelMulti(t *testing.T) { + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Microsecond) + n, err := w.Write([]byte("TestSetContextCancel: response")) + t.Logf("%s Server: wrote %d bytes", time.Now(), n) + t.Logf("%s Server: err is %v ", time.Now(), err) + }, 0) + defer ts.Close() + + // client + ctx, cancel := context.WithCancel(context.Background()) + client := resty.New().R().SetContext(ctx) + go func() { + time.Sleep(1 * time.Microsecond) + cancel() + }() + + // first + _, err := client.Get(ts.URL + "/get") + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } + + // second + _, err = client.Get(ts.URL + "/get") + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } +} + +// test context: cancel with chan +func TestSetContextCancelWithChan(t *testing.T) { + ch := make(chan struct{}) + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + defer func() { + ch <- struct{}{} // tell test request is finished + }() + t.Logf("%s Server: %v %v", time.Now(), r.Method, r.URL.Path) + ch <- struct{}{} // tell test request is canceld + t.Logf("%s Server: call canceld", time.Now()) + + <-ch // wait for client to finish request + n, err := w.Write([]byte("TestSetContextCancel: response")) + // FIXME? test server doesn't handle request cancellation + t.Logf("%s Server: wrote %d bytes", time.Now(), n) + t.Logf("%s Server: err is %v ", time.Now(), err) + + }, 0) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + <-ch // wait for server to start request handling + cancel() + }() + + _, err := resty.New().R().SetContext(ctx).Get(ts.URL + "/get") + t.Logf("%s:client:is canceled", time.Now()) + + ch <- struct{}{} // tell server to continue request handling + t.Logf("%s:client:tell server to continue", time.Now()) + + <-ch // wait for server to finish request handling + + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } +} + +// test with trace context +func TestContextWithTrace(t *testing.T) { + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("TestSetContextWithTrace: response")) + }, 0) + defer ts.Close() + + //1. Create Trace context + traceInfo := struct { + dnsDone time.Time + connectDone time.Time + }{} + + trace := &httptrace.ClientTrace{ + ConnectStart: func(network, addr string) { + traceInfo.dnsDone = time.Now() + t.Log(time.Now(), "ConnectStart:", "network=", network, ",addr=", addr) + }, + ConnectDone: func(network, addr string, err error) { + traceInfo.connectDone = time.Now() + t.Log(time.Now(), "ConnectDone:", "network=", network, ",addr=", addr) + }, + } + ctx := httptrace.WithClientTrace(context.Background(), trace) + + //2. Send request with Trace context + session := resty.New().R().SetContext(ctx) + params := MapString{"name": "ahuigo", "page": "1"} + _, err := session.SetQueryParams(params).Get(ts.URL+"/get") + if err != nil { + t.Fatal(err) + } + if traceInfo.connectDone.Sub(traceInfo.dnsDone) <= 0 { + t.Fatal("Bad trace info") + } + +} diff --git a/examples/cookie_test.go b/examples/cookie_test.go new file mode 100644 index 00000000..cf71fad7 --- /dev/null +++ b/examples/cookie_test.go @@ -0,0 +1,159 @@ +package examples + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" +) + +func TestSendCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + data := struct { + Cookies struct{ Token string } + }{} + + resp, err := resty.New().R().SetResult(&data).SetHeader("Cookie","token=1234").Get(ts.URL + "/cookie/count") + if err != nil { + panic(err) + } + if data.Cookies.Token != "1234" { + t.Errorf("Can not read cookie from response:%s", resp.String()) + } + +} + +// Test session Cookie +func TestSessionCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + result := struct { + Cookies struct{ + Count string + Name1 string + Name2 string + } + }{} + cookie1 := &http.Cookie{ + Name: "name1", + Value: "value1", + Path: "/", + } + cookie2 := &http.Cookie{ + Name: "name2", + Value: "value2", + } + session := resty.New().SetDebug(true).R() + + // 1. set cookie1 + session.SetCookie(cookie1).Get(ts.URL + "/cookie/count") + + // 2. set cookie2 and get all cookies + resp, err := session.SetCookie(cookie2).SetResult(&result).Get(ts.URL+"/cookie/count") + if err != nil { + t.Fatal(err) + } + cookies := map[string]string{} + // cookies's type is `[]*http.Cookies` + for _, c := range resp.Cookies() { + if _, exists := cookies[c.Name]; exists { + t.Fatal("duplicated cookie:", c.Name, c.Value) + } + cookies[c.Name] = c.Value + } + if cookies["count"] != "2" { + t.Fatalf("cookie count is not 2(%+v)", resp.Cookies()) + } + + if result.Cookies.Name1 != "value1" || result.Cookies.Name2 != "value2" { + t.Fatalf("Failed to send valid cookie(%+v)", resp.Cookies()) + } + +} + +// Test session Cookie +func TestSessionCookieWithClone(t *testing.T) { + ts := createHttpbinServer(0) + url := ts.URL + "/cookie/count" + defer ts.Close() + + cookie1 := &http.Cookie{ + Name: "name1", + Value: "value1", + Path: "/", + } + cookie2 := &http.Cookie{ + Name: "name2", + Value: "value2", + } + session := resty.New().R() + + // 1. set cookie1 + session.SetCookie(cookie1).Get(url) + + // 2. set cookie2 and get all cookies + resp, err := session.SetCookie(cookie2).Get(url) + if err != nil { + t.Fatal(err) + } + cookies := map[string]string{} + // cookies's type is `[]*http.Cookies` + for _, c := range resp.Cookies() { + if _, exists := cookies[c.Name]; exists { + t.Fatal("duplicated cookie:", c.Name, c.Value) + } + cookies[c.Name] = c.Value + } + if resp.GetCookie("name1") != "value1" || resp.GetCookie("name2") != "value2" { + t.Fatalf("Failed to send valid cookie(%+v)", resp.Cookies()) + } + +} + +// Test Set-Cookie +func TestResponseCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // resp, err := requests.Get("https://httpbin.org/json") + + session := resty.New().R() + resp, err := session.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + + cs := resp.Cookies() + if len(cs) == 0 { + t.Fatalf("require cookies, body=%s", resp.Body()) + } +} + +func TestResponseBuildCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + session := resty.New().R() + resp, err := session.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + + // build new resposne + cs := resp.Cookies() + if len(cs) == 0 { + t.Fatalf("require cookies, headers=%#v, body=%s", resp.Header(), resp.Body()) + } + findCount := false + for _, c := range cs { + if c.Name == "count" && c.Value == "1" { + findCount = true + } + } + if !findCount { + t.Fatalf("could not find cookie, headers=%#v", resp.Header()) + } +} diff --git a/examples/debug_test.go b/examples/debug_test.go new file mode 100644 index 00000000..9a87933d --- /dev/null +++ b/examples/debug_test.go @@ -0,0 +1,49 @@ +package examples + +import ( + "net/http" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + + +func TestDebugCurl(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R().SetBody(MapString{ + "name": "Alex", + }).SetCookies( + []*http.Cookie{ + { Name: "count", Value: "1", }, + }, + ).EnableCurl() + curl1 := request.GetCurlCmd() + if !strings.Contains(curl1, "Cookie: count=1") || !strings.Contains(curl1, "curl -X POST") { + t.Fatal("bad curl:", curl1) + } + + resp, err := request.Post(ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + //debug curl requests + curl := resp.Request.GetCurlCmd() + if !strings.Contains(curl, "Cookie: count=1") || !strings.Contains(curl, "curl -X POST") { + t.Fatal("bad curl:", curl) + } +} + +func TestDebugRequestAndResponse(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + client := resty.New().SetDebug(true) + req := client.R().SetBody(MapString{ "name": "Alex", }) + _, err := req.Post(ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/delete_test.go b/examples/delete_test.go new file mode 100644 index 00000000..010ac319 --- /dev/null +++ b/examples/delete_test.go @@ -0,0 +1,28 @@ +package examples + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Delete Form Request +func TestDeleteForm(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := MapString{ + "name": "Alex", + } + data := struct { + Body string + }{} + + r:=resty.New().R().SetBody(&json).SetResult(&data) + resp, err := r.Delete(ts.URL+"/delete") + if err == nil { + fmt.Println(resp.String()) + } + +} diff --git a/examples/error_test.go b/examples/error_test.go new file mode 100644 index 00000000..923d52a3 --- /dev/null +++ b/examples/error_test.go @@ -0,0 +1,63 @@ +package examples + +import ( + "errors" + "net/url" + "strings" + "testing" + + "context" + + "github.com/go-resty/resty/v2" +) + +func TestErrorConnnect(t *testing.T) { + _, err := resty.New().R().Get("http://127.0.0.1:12346/connect-refused") + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + if !strings.Contains(err2.Error(), "connection refused") { + t.Fatalf("not expected connnect error:%+v", err2) + } +} + +func TestErrorTimeout(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // resp, err := resty.New().Get("https://httpbin.org/json") + _, err := resty.New().SetTimeout(1).R().Get(ts.URL + "/sleep/10") + + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + + if !strings.Contains(err2.Error(), "context deadline exceeded") { + t.Fatalf("unexpected error:%+v", err2) + } + +} + +func TestErrorURL(t *testing.T) { + _, err := resty.New().R().Get("xxxx") + + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + + if err2.Op != "parse" { + t.Fatalf("unexpected error:%+v", err2) + } +} + +func errIsContextCancel(err error) bool { + var ue *url.Error + ok := errors.As(err, &ue) + if !ok { + return false + } + return ue.Err == context.Canceled +} diff --git a/examples/get_test.go b/examples/get_test.go new file mode 100644 index 00000000..31f90944 --- /dev/null +++ b/examples/get_test.go @@ -0,0 +1,95 @@ +package examples + +import ( + "net/url" + "testing" + + "github.com/go-resty/resty/v2" +) + +var client = resty.New() + +// Get example: fetch json response +func TestGetJson(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // resp, err := requests.Get("https://httpbin.org/json") + var json map[string]interface{} + _, err := resty.New().R().SetResult(&json).Get(ts.URL + "/get") + if err == nil { + t.Logf("response json:%#v\n", json) + } + if err != nil { + t.Fatal(err) + } +} + +// Get with params +func TestGetParams(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + params := map[string]string{"name": "Alex", "page": "1"} + resp, err := client.R().SetQueryParams(params).SetResult(&json).Get(ts.URL + "/get") + + if err != nil { + t.Fatal(err) + } + if err == nil { + if json.Args["name"] != "Alex" { + t.Fatalf("bad json:%s", string(resp.Body())) + } + } +} + +type MapString= map[string]string + + +// Support array args like: ids=id1&ids=id2&ids=id3 +func TestGetParamArray(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + paramsArray := url.Values{ + "ids": []string{"id1", "id2"}, + } + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + resp, err := client.R().SetQueryParamsFromValues(paramsArray).SetResult(&json).Get(ts.URL + "/get") + + if err != nil { + t.Fatal(err) + } + if err == nil { + if json.Args["ids"] != "id1,id2" { + t.Fatal("Invalid response: " + string(resp.Body())) + } + } +} + +func TestGetWithHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + params := MapString{"name": "Alex"} + resp, err := client.R().SetResult(&json).SetQueryParams(params).Get(ts.URL+"/get",) + + if err != nil { + t.Fatal(err) + } + if err == nil { + t.Log(string(resp.Body())) + } +} diff --git a/examples/post_file_test.go b/examples/post_file_test.go new file mode 100644 index 00000000..274fc579 --- /dev/null +++ b/examples/post_file_test.go @@ -0,0 +1,47 @@ +package examples + +import ( + "path/filepath" + "testing" + + "github.com/go-resty/resty/v2" +) + +/* +An example about post both `file` and `form data`: +curl "https://www.httpbin.org/post" -F 'file1=@./test-file.txt' -F 'file2=@./version' -F 'name=alex' +*/ +func TestPostFile(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + Files struct { + File1 string + } + Form struct { + Name string + } + }{} + r:=resty.New().R().SetFormData(MapString{ + "name": "Alex", + }). + SetFile("file1", filepath.Join(getTestDataPath(),"text-file.txt")). + SetResult(&data) + + // 2. post file + resp, err := r.Post( ts.URL+"/file",) + if err != nil { + t.Fatal(err) + } + + // 3. check response + if data.Files.File1 == "" { + t.Error("invalid response files:", resp.String()) + } + if data.Form.Name == "" { + t.Error("invalid response forms:", resp.String()) + } + +} diff --git a/examples/post_test.go b/examples/post_test.go new file mode 100644 index 00000000..f6e99db9 --- /dev/null +++ b/examples/post_test.go @@ -0,0 +1,165 @@ +package examples + +import ( + ejson "encoding/json" + "net/url" + "path/filepath" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Post Params: use with content-type: none +// curl -X POST "https://www.httpbin.org/post?name=Alex" +func TestPostParams(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Args struct { + Name string + } + }{} + resp, err := resty.New().R().SetResult(&data).SetQueryParams(MapString{"name":"Alex"}).Post( + ts.URL+"/post", + ) + if err != nil { + t.Fatal(err) + } + if data.Args.Name != "Alex" { + t.Fatal("invalid response body:", resp.String()) + } +} + +// Post Datas: use
with application/x-www-form-urlencoded +// curl -H 'Content-Type: application/x-www-form-urlencoded' https://www.httpbin.org/post -d 'name=Alex' +func TestPostFormUrlEncode(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New().R().SetFormDataFromValues(url.Values{ + "name": []string{"Alex"}, + }).SetResult(&data) + resp, err := r.Post( ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + if data.Body != "name=Alex" { + t.Fatal("invalid response body:", resp.String()) + } +} + +// POST FormData: multipart/form-data; boundary=.... +// curl https://www.httpbin.org/post -F 'name=Alex' -F "file1=@./testdata/text-file.txt" +func TestPostFormData(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New().R().SetFormData(MapString{ + "name": "Alex", + }).SetFile("file1", filepath.Join(getTestDataPath(),"text-file.txt")). + SetResult(&data) + resp, err := r.Post( ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(data.Body, "form-data; name=\"name\"\r\n\r\nAlex\r\n") { + t.Error("invalid response body:", resp.String()) + } +} + +// POST Json: application/json +// curl -H "Content-Type: application/json" https://www.httpbin.org/post -d '{"name":"Alex"}' +func TestPostJson(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := MapString{ + "name": "Alex", + } + data := struct { + Body string + }{} + r:=resty.New().R().SetBody(json).SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + + + // is expected results + jsonData, _ := ejson.Marshal(json) // if data.Data!= "{\"name\":\"Alex\"}"{ + if data.Body != string(jsonData) { + t.Error("invalid response body:", resp.String()) + } +} + +// Post Raw Bypes: text/plain(default) +// curl -H "Content-Type: text/plain" https://www.httpbin.org/post -d 'raw data: Hi, Jack!' +func TestRawBytes(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + rawText := "raw data: Hi, Jack!" + var data = struct { + Body string + }{} + r:=resty.New().R().SetBody([]byte(rawText)).SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + if data.Body != rawText { + t.Error("invalid response body:", resp.String()) + } +} + +// Post Raw String: text/plain +// curl -H "Content-Type: text/plain" http://0:4500/post -d 'raw data: Hi, Jack!' +func TestRawString(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data interface{} + rawText := "raw data: Hi, Jack!" + r:=resty.New().R().SetHeader("Content-Type", "text/plain").SetBody([]byte(rawText)).SetResult(&data) + resp, err := r.Post(ts.URL+"/post", ) + if err != nil { + t.Fatal(err) + } + if data.(map[string]interface{})["body"].(string) != rawText { + t.Error("invalid response body:", resp.String()) + } +} + + +// TestPostEncodedString: application/x-www-form-urlencoded +// curl -H 'Content-Type: application/x-www-form-urlencoded' http://0:4500/post -d 'name=Alex&age=29' +func TestPostEncodedString(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New(). + SetDebug(true). + R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetBody("name=Alex&age=29"). + SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + if data.Body != "name=Alex\u0026age=29" { + t.Error("invalid response body:", resp.String()) + } +} diff --git a/examples/proxy_test.go b/examples/proxy_test.go new file mode 100644 index 00000000..fcdec34f --- /dev/null +++ b/examples/proxy_test.go @@ -0,0 +1,14 @@ +package examples + +import ( + "testing" +) + +func TestProxy(t *testing.T) { + println("5. Get: with proxy") + /* todo + session := requests.R() + session.SetProxy("http://192.168.1.190:8888") + session.Get("https://www.httpbin.org/cookies/set?freeform=1234") + */ +} diff --git a/examples/req_header_test.go b/examples/req_header_test.go new file mode 100644 index 00000000..958cdef2 --- /dev/null +++ b/examples/req_header_test.go @@ -0,0 +1,33 @@ +package examples + +import ( + "testing" + + "github.com/go-resty/resty/v2" +) + +// Send headers +func TestSendHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := struct{ + Args struct{ + Name string + Age string `json:"age"` + } + }{} + _, err := resty.New(). + SetDebug(true). + R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetQueryString("name=Alex&age=29"). + SetResult(&json). + Get(ts.URL + "/get") + if err != nil { + t.Fatal(err) + } + if json.Args.Age != "29" { + t.Fatalf("invalid json:%v\n", json) + } +} diff --git a/examples/response_build_test.go b/examples/response_build_test.go new file mode 100644 index 00000000..bdfffaa0 --- /dev/null +++ b/examples/response_build_test.go @@ -0,0 +1,40 @@ +package examples + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/go-resty/resty/v2" +) + +func TestResponseBuilder(t *testing.T) { + var err error + var data = 1 + responseBytes, _ := json.Marshal(data) + + respRecorder := httptest.NewRecorder() + respRecorder.Write(responseBytes) + + request := resty.New().R() + // build response + resp := resty.Response{ + Request: request, + RawResponse :respRecorder.Result(), + // body: []byte("abc"), + } + // if resp.body, err = io.ReadAll(resp.RawResponse.Body); err != nil { + // t.Fatalf("err:%v", err) + // } + ndata, err := io.ReadAll(resp.RawResponse.Body) + if err != nil { + t.Fatalf("err:%v", err) + } + + if !bytes.Equal(ndata , responseBytes) { + t.Fatalf("expect response:%v", data) + } + +} diff --git a/examples/response_test.go b/examples/response_test.go new file mode 100644 index 00000000..c8b844cf --- /dev/null +++ b/examples/response_test.go @@ -0,0 +1,55 @@ +package examples + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Test Response +func TestResponse(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + fmt.Println("Status Code:", resp.StatusCode()) + fmt.Println("Time:", resp.Time()) + fmt.Println("Size:", resp.Size()) + fmt.Println("Headers:") + for key, value := range resp.Header() { + fmt.Println(key, "=", value) + } + fmt.Println("Cookies:") + for i, cookie := range resp.Cookies() { + fmt.Printf("cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value) + } + +} + +// Test response headers +func TestResponseHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + + if resp.Header().Get("content-type") != "application/json" { + t.Fatal("bad response header") + } + + println("content-type:", resp.Header().Get("content-type")) +} + +// Test response body +func TestResponseBody(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + println(resp.Body()) + println(resp.String()) +} diff --git a/examples/retry_test.go b/examples/retry_test.go new file mode 100644 index 00000000..d5980e7d --- /dev/null +++ b/examples/retry_test.go @@ -0,0 +1,76 @@ +package examples + +import ( + "encoding/json" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +func TestRetryCondition(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // retry 3 times + maxRetries := 2 + r := resty.New(). + SetRetryCount(maxRetries). + SetRetryWaitTime(time.Microsecond). + SetRetryMaxWaitTime(time.Microsecond). + AddRetryCondition( + func(r *resty.Response, _ error) bool { + var data map[string]interface{} + err:=json.Unmarshal(r.Body(), &data) + if err != nil { + return true + } + return data["headers"] != "a" + }, + ) .R() + + var data struct{ + Body string + Method string + } + resp, err := r.SetBody([]byte("alex")).SetResult(&data).Post(ts.URL+"/post", ) + if err != nil { + t.Fatal(err, resp.String()) + } + + if resp.Request.Attempt != maxRetries+1 { + t.Fatalf("Attempt %d, expected: %d", resp.Request.Attempt, maxRetries+1) + } + + if data.Body != "alex" { + t.Fatalf("Bad response body:%s", resp.String()) + } + if data.Method != "POST" { + t.Fatalf("Bad request method:%s", resp.String()) + } +} + +// func TestRetryConditionFalse(t *testing.T) { +// ts := createHttpbinServer(0) +// defer ts.Close() + +// // retry 3 times +// r := requests.R(). +// SetRetryCount(3). +// SetRetryCondition(func(resp *requests.Response, err error) bool { +// return false +// }) + +// resp, err := r.Get(ts.URL + "/get") +// if err != nil { +// t.Fatal(err) +// } + +// if resp.Attempt != 0 { +// t.Fatalf("Attemp %d not equal to %d", resp.Attempt, 0) +// } + +// var json map[string]interface{} +// resp.Json(&json) +// t.Logf("response json:%#v\n", json["headers"]) +// } diff --git a/examples/server_test.go b/examples/server_test.go new file mode 100644 index 00000000..98cc7db7 --- /dev/null +++ b/examples/server_test.go @@ -0,0 +1,249 @@ +package examples + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + ioutil "io" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strconv" + "strings" + "time" +) + +const maxMultipartMemory = 4 << 30 // 4MB + +// tlsCert: +// +// 0 no certificate +// 1 with self-signed certificate +// 2 with custom certificate from CA +func createHttpbinServer(tlsCert int) (ts *httptest.Server) { + ts = createTestServer(func(w http.ResponseWriter, r *http.Request) { + switch path := r.URL.Path; { + case path == "/get": + getHandler(w, r) + case path == "/post": + postHandler(w, r) + case path == "/delete": + postHandler(w, r) + case path == "/file": + fileHandler(w, r) + case strings.HasPrefix(path, "/sleep/"): //sleep/3 + sleepHandler(w, r) + case path == "/cookie/count": + cookieHandler(w, r) + default: + _, _ = w.Write([]byte("404 " + path)) + } + }, tlsCert) + + return ts +} + +func postHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "body": string(body), + "method": r.Method, + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + + +func fileHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := r.ParseMultipartForm(maxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + panic(fmt.Sprintf("error on parse multipart form array: %v", err)) + } + } + // parse form data + formData := make(map[string]string) + for k, vs := range r.PostForm { + for _, v := range vs { + formData[k] = v + } + } + // parse files + files := make(map[string]string) + if r.MultipartForm != nil && r.MultipartForm.File != nil { + for key, fhs := range r.MultipartForm.File { + // if len(fhs)>0 + // f, err := fhs[0].Open() + files[key] = fhs[0].Filename + } + } + + //output + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "form": formData, + "files": files, + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + +func sleepHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + regx := regexp.MustCompile(`^/sleep/(\d+)`) + res := regx.FindStringSubmatch(r.URL.Path) // res may be: []string(nil) + miliseconds := 0 + if res != nil { + miliseconds, _ = strconv.Atoi(res[1]) + } + time.Sleep(time.Duration(miliseconds) * time.Microsecond) + out := fmt.Sprintf("sleep %d ms", miliseconds) + _, _ = w.Write([]byte(out)) +} + +func getHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "body": string(body), + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + +func cookieHandler(w http.ResponseWriter, r *http.Request) { + + switch r.URL.Path { + case "/cookie/count": + reqCookies := map[string]string{} + for _, c := range r.Cookies() { + reqCookies[c.Name] = c.Value + } + + count := "1" + cookie, err := r.Cookie("count") + if err == nil { + i, _ := strconv.Atoi(cookie.Value) + count = strconv.Itoa(i + 1) + } + http.SetCookie(w, &http.Cookie{Name: "count", Value: url.QueryEscape(count)}) + w.Header().Set("Content-Type", "application/json") + + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "args": parseRequestArgs(r), + "body": string(body), + "count": count, + "cookies": reqCookies, + "headers": dumpRequestHeader(r), + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) + default: + _, _ = w.Write([]byte("404 " + r.URL.Path)) + } +} + +func dumpRequestHeader(req *http.Request) string { + var res strings.Builder + headers := sortHeaders(req) + for _, kv := range headers { + res.WriteString(kv[0] + ": " + kv[1] + "\n") + } + return res.String() +} + +// sortHeaders +func sortHeaders(request *http.Request) [][2]string { + headers := [][2]string{} + for k, vs := range request.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return headers +} + +func createEchoServer() (ts *httptest.Server) { + ts = createTestServer(func(w http.ResponseWriter, r *http.Request) { + res := dumpRequest(r) + _, _ = w.Write([]byte(res)) + }, 0) + + return ts +} +func parseRequestArgs(request *http.Request) map[string]string { + query := request.URL.RawQuery + params := map[string]string{} + paramsList, _ := url.ParseQuery(query) + for key, vals := range paramsList { + // params[key] = vals[len(vals)-1] + params[key] = strings.Join(vals, ",") + } + return params +} + +func dumpRequest(request *http.Request) string { + var r strings.Builder + // dump header + res := request.Method + " " + //request.URL.String() +" "+ + request.Host + + request.URL.Path + "?" + request.URL.RawQuery + " " + request.Proto + " " + + "\n" + r.WriteString(res) + r.WriteString(dumpRequestHeader(request)) + r.WriteString("\n") + + // dump body + buf, _ := ioutil.ReadAll(request.Body) + request.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) // important!! + r.WriteString(string(buf)) + return r.String() +} + +/* +* + - tlsCert: + 0 no certificate + 1 with self-signed certificate + 2 with custom certificate from CA +*/ +func createTestServer(fn func(w http.ResponseWriter, r *http.Request), tlsCert int) (ts *httptest.Server) { + if tlsCert == 0 { + // 1. http test server + ts = httptest.NewServer(http.HandlerFunc(fn)) + } else { + // 2. https test server: https://stackoverflow.com/questions/54899550/create-https-test-server-for-any-client + ts = httptest.NewUnstartedServer(http.HandlerFunc(fn)) + + // 3. use own cert + if tlsCert == 2 { + cert, err := tls.LoadX509KeyPair("./conf/nginx.crt", "./conf/nginx.key") + if err != nil { + panic(err) + } + _ = cert + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + } + ts.StartTLS() + } + return ts +} diff --git a/examples/timeout_test.go b/examples/timeout_test.go new file mode 100644 index 00000000..c36f46fd --- /dev/null +++ b/examples/timeout_test.go @@ -0,0 +1,20 @@ +package examples + +import ( + "strings" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +func TestTimeout(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + req := resty.New().SetTimeout(1*time.Microsecond).R() + _, err:= req.Get(ts.URL+"/sleep/2") + assertNotEqual(t, nil, err) + assertEqual(t, true, strings.Contains(err.Error(), "Client.Timeout exceeded")) + +} diff --git a/examples/utils_test.go b/examples/utils_test.go new file mode 100644 index 00000000..1afc0ff8 --- /dev/null +++ b/examples/utils_test.go @@ -0,0 +1,57 @@ +package examples + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "testing" +) + +func getTestDataPath() string { + pwd, _ := os.Getwd() + return filepath.Join(pwd, "../.testdata") +} + + +func assertType(t *testing.T, typ, v interface{}) { + if reflect.DeepEqual(reflect.TypeOf(typ), reflect.TypeOf(v)) { + t.Errorf("Expected type %t, got %t", typ, v) + } +} + +func assertError(t *testing.T, err error) { + if err != nil { + t.Errorf("Error occurred [%v]", err) + } +} + +func assertErrorIs(t *testing.T, e, g error) (r bool) { + if !errors.Is(g, e) { + t.Errorf("Expected [%v], got [%v]", e, g) + } + + return true +} + +func assertEqual(t *testing.T, e, g interface{}) (r bool) { + if !equal(e, g) { + t.Fatalf("Expected [%v], got [%v]", e, g) + } + + return +} + +func assertNotEqual(t *testing.T, e, g interface{}) (r bool) { + if equal(e, g) { + t.Errorf("Expected [%v], got [%v]", e, g) + } else { + r = true + } + + return +} + +func equal(expected, got interface{}) bool { + return reflect.DeepEqual(expected, got) +} \ No newline at end of file diff --git a/middleware.go b/middleware.go index 15cbc49a..e69ff8f4 100644 --- a/middleware.go +++ b/middleware.go @@ -307,6 +307,13 @@ func addCredentials(c *Client, r *Request) error { return nil } +func createCurlCmd(c *Client, r *Request) (err error) { + if r.enableCurl{ + r.curlCmd = BuildCurlRequest(r.RawRequest, c.httpClient.Jar) + } + return nil +} + func requestLogger(c *Client, r *Request) error { if r.Debug { rr := r.RawRequest diff --git a/request.go b/request.go index fec09763..168832bd 100644 --- a/request.go +++ b/request.go @@ -71,6 +71,20 @@ type Request struct { multipartFiles []*File multipartFields []*MultipartField retryConditions []RetryConditionFunc + enableCurl bool + curlCmd string +} + +func (r *Request) EnableCurl() *Request{ + r.enableCurl = true + return r +} + +func (r *Request) GetCurlCmd() string{ + if r.curlCmd == ""{ + r.curlCmd = BuildCurlRequest(r.RawRequest, r.client.httpClient.Jar) + } + return r.curlCmd } // Context method returns the Context if its already set in request diff --git a/response.go b/response.go index 63c95c41..5fda2571 100644 --- a/response.go +++ b/response.go @@ -103,6 +103,15 @@ func (r *Response) Cookies() []*http.Cookie { return r.RawResponse.Cookies() } +func (r *Response) GetCookie(key string) (val string) { + cookies := map[string]string{} + for _, c := range r.Cookies() { + cookies[c.Name] = c.Value + } + val = cookies[key] + return val +} + // String method returns the body of the server response as String. func (r *Response) String() string { if len(r.body) == 0 { diff --git a/shellescape/shellescape.go b/shellescape/shellescape.go new file mode 100644 index 00000000..3d6b5888 --- /dev/null +++ b/shellescape/shellescape.go @@ -0,0 +1,34 @@ +/* +Package shellescape provides the shellescape.Quote to escape arbitrary +strings for a safe use as command line arguments in the most common +POSIX shells. + +The original Python package which this work was inspired by can be found +at https://pypi.python.org/pypi/shellescape. +*/ +package shellescape // "import gopkg.in/alessio/shellescape.v1" + +import ( + "regexp" + "strings" +) + +var pattern *regexp.Regexp + +func init() { + pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) +} + +// Quote returns a shell-escaped version of the string s. The returned value +// is a string that can safely be used as one token in a shell command line. +func Quote(s string) string { + if len(s) == 0 { + return "''" + } + + if pattern.MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + + return s +} diff --git a/util_curl.go b/util_curl.go new file mode 100644 index 00000000..aa724e48 --- /dev/null +++ b/util_curl.go @@ -0,0 +1,72 @@ +package resty + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/cookiejar" + + "net/url" + "strings" + + "github.com/go-resty/resty/v2/shellescape" +) + +func BuildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) { + // 1. generate curl request + curl = "curl -X " + req.Method + " " + // req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " " + headers := getHeaders(req) + for _, kv := range *headers { + curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` ` + } + + // 1.2 generate curl with cookies + if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok{ + cookies := cookieJar.Cookies(req.URL) + if len(cookies) > 0 { + curl += ` -H ` + shellescape.Quote(dumpCookies(cookies)) + " " + } + } + + + // body + if req.Body != nil { + buf, _ := ioutil.ReadAll(req.Body) + req.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) // important!! + curl += `-d ` + shellescape.Quote(string(buf)) + } + + curl += " " + shellescape.Quote(req.URL.String()) + return curl +} + +func dumpCookies(cookies []*http.Cookie) string { + sb := strings.Builder{} + sb.WriteString("Cookie: ") + for _, cookie := range cookies { + sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&") + } + return strings.TrimRight(sb.String(), "&") +} + +// getHeaders +func getHeaders(req *http.Request) *[][2]string { + headers := [][2]string{} + for k, vs := range req.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return &headers +}