save req and res with basic types only
This commit is contained in:
parent
604aa43eb5
commit
d907a55566
39
capture.go
39
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,
|
||||
}
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,5 +1,3 @@
|
|||
module github.com/ofabricio/capture
|
||||
|
||||
require github.com/ofabricio/curl v0.1.0
|
||||
|
||||
go 1.13
|
||||
|
|
2
go.sum
2
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=
|
149
main.go
149
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()
|
||||
}
|
||||
|
|
114
main_test.go
114
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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue