save req and res with basic types only

This commit is contained in:
Fabricio 2019-11-16 12:47:02 -03:00
parent 604aa43eb5
commit d907a55566
5 changed files with 139 additions and 167 deletions

View file

@ -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,
}
}

2
go.mod
View file

@ -1,5 +1,3 @@
module github.com/ofabricio/capture
require github.com/ofabricio/curl v0.1.0
go 1.13

2
go.sum
View file

@ -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=

149
main.go
View file

@ -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()
}

View file

@ -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()
}