From d907a55566ed06caa18f8ca0cb29a78145a738a7 Mon Sep 17 00:00:00 2001 From: Fabricio Date: Sat, 16 Nov 2019 12:47:02 -0300 Subject: [PATCH] save req and res with basic types only --- capture.go | 39 ++++++++++---- go.mod | 2 - go.sum | 2 - main.go | 149 ++++++++++++++++++++++++++------------------------- main_test.go | 114 +++++++++++++-------------------------- 5 files changed, 139 insertions(+), 167 deletions(-) diff --git a/capture.go b/capture.go index 6cff8c8..a5e2b0a 100644 --- a/capture.go +++ b/capture.go @@ -20,12 +20,36 @@ type CaptureService struct { // Capture is our traffic data type Capture struct { ID int - Req *http.Request - Res *http.Response + Req Req + Res Res // Elapsed time of the request, in milliseconds Elapsed time.Duration } +// CaptureDump is all the dumps shown in the dashboard +type CaptureDump struct { + Request string `json:"request"` + Response string `json:"response"` + Curl string `json:"curl"` +} + +type Req struct { + Proto string + Method string + Url string + Path string + Header http.Header + Body []byte +} + +type Res struct { + Proto string + Status string + Code int + Header http.Header + Body []byte +} + // DashboardItem is an item in the dashboard's list type DashboardItem struct { ID int `json:"id"` @@ -36,13 +60,6 @@ type DashboardItem struct { Elapsed time.Duration `json:"elapsed"` } -// CaptureDump is all the dumps shown in the dashboard -type CaptureDump struct { - Request string `json:"request"` - Response string `json:"response"` - Curl string `json:"curl"` -} - // NewCaptureService creates a new service of captures func NewCaptureService(maxItems int) *CaptureService { return &CaptureService{ @@ -97,9 +114,9 @@ func (s *CaptureService) DashboardItems() []DashboardItem { for i, capture := range s.items { metadatas[i] = DashboardItem{ ID: capture.ID, - Path: capture.Req.URL.Path, + Path: capture.Req.Path, Method: capture.Req.Method, - Status: capture.Res.StatusCode, + Status: capture.Res.Code, Elapsed: capture.Elapsed, } } diff --git a/go.mod b/go.mod index 1bb8a66..fb89bec 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/ofabricio/capture -require github.com/ofabricio/curl v0.1.0 - go 1.13 diff --git a/go.sum b/go.sum index 9b6e75f..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/ofabricio/curl v0.1.0 h1:ntXuBULZLQmCdAMxZNXzse069DbAKb/Flxe/2uuZuNk= -github.com/ofabricio/curl v0.1.0/go.mod h1:RtLkZIOgxjm+l0jdj04lrETzu8u5SmPPdLyGAuC4ukg= diff --git a/main.go b/main.go index bc5fa4f..edfd322 100644 --- a/main.go +++ b/main.go @@ -15,10 +15,9 @@ import ( "path" "path/filepath" "plugin" + "sort" "strings" "time" - - "github.com/ofabricio/curl" ) // StatusInternalProxyError is any unknown proxy error @@ -105,12 +104,8 @@ func NewDashboardRetryHandler(srv *CaptureService, next http.HandlerFunc) http.H id := path.Base(req.URL.Path) capture := srv.Find(id) - reqBody, _ := ioutil.ReadAll(capture.Req.Body) - req.Body.Close() - capture.Req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) - // creates a new request based on the current one - r, _ := http.NewRequest(capture.Req.Method, capture.Req.URL.String(), bytes.NewReader(reqBody)) + r, _ := http.NewRequest(capture.Req.Method, capture.Req.Url, bytes.NewReader(capture.Req.Body)) r.Header = capture.Req.Header next.ServeHTTP(rw, r) @@ -169,31 +164,50 @@ func NewPluginHandler(next http.HandlerFunc) http.HandlerFunc { // NewRecorderHandler records all the traffic data func NewRecorderHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { - // save req body for later - reqBody, _ := ioutil.ReadAll(req.Body) - req.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) + // Save req body for later. + reqBody, _ := ioutil.ReadAll(r.Body) + r.Body.Close() + r.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) rec := httptest.NewRecorder() + // Record Roundtrip. + start := time.Now() - next.ServeHTTP(rec, req) + next.ServeHTTP(rec, r) elapsed := time.Since(start).Truncate(time.Millisecond) / time.Millisecond - // respond + resBody := rec.Body.Bytes() + + // Respond to client with recorded response. + for k, v := range rec.Header() { rw.Header()[k] = v } rw.WriteHeader(rec.Code) - rw.Write(rec.Body.Bytes()) + rw.Write(resBody) - // record req and res - req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) - res := rec.Result() + // Save req and res data. + + req := Req{ + Proto: r.Proto, + Method: r.Method, + Url: r.URL.String(), + Path: r.URL.Path, + Header: r.Header, + Body: reqBody, + } + res := Res{ + Proto: rec.Result().Proto, + Status: rec.Result().Status, + Code: rec.Code, + Header: rec.Header(), + Body: resBody, + } srv.Insert(Capture{Req: req, Res: res, Elapsed: elapsed}) } } @@ -216,68 +230,55 @@ func NewProxyHandler(URL string) http.HandlerFunc { } func dump(c *Capture) CaptureDump { - reqDump, err := dumpRequest(c.Req) - if err != nil { - fmt.Printf("could not dump request: %v\n", err) + req := c.Req + res := c.Res + return CaptureDump{ + Request: dumpContent(req.Header, req.Body, "%s %s %s\n\n", req.Method, req.Path, req.Proto), + Response: dumpContent(res.Header, res.Body, "%s %s\n\n", res.Proto, res.Status), + Curl: dumpCurl(req), } - resDump, err := dumpResponse(c.Res) - if err != nil { - fmt.Printf("could not dump response: %v\n", err) - } - strcurl, err := curl.New(c.Req) - if err != nil { - fmt.Printf("could not convert request to curl: %v\n", err) - } - return CaptureDump{Request: string(reqDump), Response: string(resDump), Curl: strcurl} } -func dumpRequest(req *http.Request) ([]byte, error) { - if req.Header.Get("Content-Encoding") == "gzip" { - return dumpGzipRequest(req) +func dumpContent(header http.Header, body []byte, format string, args ...interface{}) string { + b := strings.Builder{} + fmt.Fprintf(&b, format, args...) + dumpHeader(&b, header) + b.WriteString("\n") + dumpBody(&b, header, body) + return b.String() +} + +func dumpHeader(dst *strings.Builder, header http.Header) { + var headers []string + for k, v := range header { + headers = append(headers, fmt.Sprintf("%s: %s\n", k, strings.Join(v, " "))) } - return httputil.DumpRequest(req, true) -} - -func dumpGzipRequest(req *http.Request) ([]byte, error) { - - reqBody, _ := ioutil.ReadAll(req.Body) - req.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) - - reader, _ := gzip.NewReader(bytes.NewReader(reqBody)) - req.Body = ioutil.NopCloser(reader) - reqDump, err := httputil.DumpRequest(req, true) - req.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) - return reqDump, err -} - -func dumpResponse(res *http.Response) ([]byte, error) { - if res.StatusCode == StatusInternalProxyError { - return dumpResponseBody(res) + sort.Strings(headers) + for _, v := range headers { + dst.WriteString(v) } - if res.Header.Get("Content-Encoding") == "gzip" { - return dumpGzipResponse(res) +} + +func dumpBody(dst *strings.Builder, header http.Header, body []byte) { + reqBody := body + if header.Get("Content-Encoding") == "gzip" { + reader, _ := gzip.NewReader(bytes.NewReader(body)) + reqBody, _ = ioutil.ReadAll(reader) } - return httputil.DumpResponse(res, true) + dst.Write(reqBody) } -// Dumps only the body when we have an proxy error. -// This body is set in NewProxyHandler() in proxy.ErrorHandler -func dumpResponseBody(res *http.Response) ([]byte, error) { - resBody, _ := ioutil.ReadAll(res.Body) - res.Body.Close() - res.Body = ioutil.NopCloser(bytes.NewReader(resBody)) - return resBody, nil -} - -func dumpGzipResponse(res *http.Response) ([]byte, error) { - resBody, _ := ioutil.ReadAll(res.Body) - res.Body.Close() - res.Body = ioutil.NopCloser(bytes.NewReader(resBody)) - - reader, _ := gzip.NewReader(bytes.NewReader(resBody)) - res.Body = ioutil.NopCloser(reader) - resDump, err := httputil.DumpResponse(res, true) - res.Body = ioutil.NopCloser(bytes.NewReader(resBody)) - return resDump, err +func dumpCurl(req Req) string { + var b strings.Builder + // build cmd + fmt.Fprintf(&b, "curl -X %s %s", req.Method, req.Url) + // build headers + for k, v := range req.Header { + fmt.Fprintf(&b, " \\\n -H '%s: %s'", k, strings.Join(v, " ")) + } + // build body + if len(req.Body) > 0 { + fmt.Fprintf(&b, " \\\n -d '%s'", req.Body) + } + return b.String() } diff --git a/main_test.go b/main_test.go index 9ac3b4d..3019745 100644 --- a/main_test.go +++ b/main_test.go @@ -84,91 +84,49 @@ func PostRequest() TestCase { } } -func TestDumpRequest(t *testing.T) { - msg := "hello" - - // given - req, err := http.NewRequest(http.MethodPost, "http://localhost:9000/", strings.NewReader(msg)) - if err != nil { - t.Errorf("Could not create request: %v", err) +func ExampleDump() { + c := &Capture{ + Req: Req{ + Proto: "HTTP/1.1", + Url: "http://localhost/hello", + Path: "/hello", + Method: "GET", + Header: map[string][]string{"Content-Encoding": {"none"}}, + Body: []byte(`hello`), + }, + Res: Res{ + Proto: "HTTP/1.1", + Header: map[string][]string{"Content-Encoding": {"gzip"}}, + Body: gzipStr("gziped hello"), + Status: "200 OK", + }, } + got := dump(c) - // when - body, err := dumpRequest(req) + fmt.Println(got.Request) + fmt.Println(got.Response) + fmt.Println(got.Curl) - // then - if err != nil { - t.Errorf("Dump Request error: %v", err) - } - if !strings.Contains(string(body), msg) { - t.Errorf("Dump Request is not '%s'", msg) - } + // Output: + // GET /hello HTTP/1.1 + // + // Content-Encoding: none + // + // hello + // HTTP/1.1 200 OK + // + // Content-Encoding: gzip + // + // gziped hello + // curl -X GET http://localhost/hello \ + // -H 'Content-Encoding: none' \ + // -d 'hello' } -func TestDumpRequestGzip(t *testing.T) { - msg := "hello" - - // given - req, err := http.NewRequest(http.MethodPost, "http://localhost:9000/", strings.NewReader(gzipStr(msg))) - req.Header.Set("Content-Encoding", "gzip") - if err != nil { - t.Errorf("Could not create request: %v", err) - } - - // when - body, err := dumpRequest(req) - - // then - if err != nil { - t.Errorf("Dump Request Gzip error: %v", err) - } - if !strings.Contains(string(body), msg) { - t.Errorf("Dump Request Gzip is not '%s'", msg) - } -} - -func TestDumpResponse(t *testing.T) { - msg := "hello" - - // given - res := &http.Response{Body: ioutil.NopCloser(strings.NewReader(msg))} - - // when - body, err := dumpResponse(res) - - // then - if err != nil { - t.Errorf("Dump Response Error: %v", err) - } - if !strings.Contains(string(body), msg) { - t.Errorf("Dump Response is not '%s'", msg) - } -} - -func TestDumpResponseGzip(t *testing.T) { - msg := "hello" - - // given - h := make(http.Header) - h.Set("Content-Encoding", "gzip") - res := &http.Response{Header: h, Body: ioutil.NopCloser(strings.NewReader(gzipStr(msg)))} - - // when - body, err := dumpResponse(res) - - // then - if err != nil { - t.Errorf("Dump Response error: %v", err) - } - if !strings.Contains(string(body), msg) { - t.Error("Not hello") - } -} - -func gzipStr(str string) string { +func gzipStr(str string) []byte { var buff bytes.Buffer g := gzip.NewWriter(&buff) io.WriteString(g, str) g.Close() - return buff.String() + return buff.Bytes() }