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
|
// Capture is our traffic data
|
||||||
type Capture struct {
|
type Capture struct {
|
||||||
ID int
|
ID int
|
||||||
Req *http.Request
|
Req Req
|
||||||
Res *http.Response
|
Res Res
|
||||||
// Elapsed time of the request, in milliseconds
|
// Elapsed time of the request, in milliseconds
|
||||||
Elapsed time.Duration
|
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
|
// DashboardItem is an item in the dashboard's list
|
||||||
type DashboardItem struct {
|
type DashboardItem struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
@ -36,13 +60,6 @@ type DashboardItem struct {
|
||||||
Elapsed time.Duration `json:"elapsed"`
|
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
|
// NewCaptureService creates a new service of captures
|
||||||
func NewCaptureService(maxItems int) *CaptureService {
|
func NewCaptureService(maxItems int) *CaptureService {
|
||||||
return &CaptureService{
|
return &CaptureService{
|
||||||
|
@ -97,9 +114,9 @@ func (s *CaptureService) DashboardItems() []DashboardItem {
|
||||||
for i, capture := range s.items {
|
for i, capture := range s.items {
|
||||||
metadatas[i] = DashboardItem{
|
metadatas[i] = DashboardItem{
|
||||||
ID: capture.ID,
|
ID: capture.ID,
|
||||||
Path: capture.Req.URL.Path,
|
Path: capture.Req.Path,
|
||||||
Method: capture.Req.Method,
|
Method: capture.Req.Method,
|
||||||
Status: capture.Res.StatusCode,
|
Status: capture.Res.Code,
|
||||||
Elapsed: capture.Elapsed,
|
Elapsed: capture.Elapsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,5 +1,3 @@
|
||||||
module github.com/ofabricio/capture
|
module github.com/ofabricio/capture
|
||||||
|
|
||||||
require github.com/ofabricio/curl v0.1.0
|
|
||||||
|
|
||||||
go 1.13
|
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"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"plugin"
|
"plugin"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ofabricio/curl"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusInternalProxyError is any unknown proxy error
|
// 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)
|
id := path.Base(req.URL.Path)
|
||||||
capture := srv.Find(id)
|
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
|
// 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
|
r.Header = capture.Req.Header
|
||||||
|
|
||||||
next.ServeHTTP(rw, r)
|
next.ServeHTTP(rw, r)
|
||||||
|
@ -169,31 +164,50 @@ func NewPluginHandler(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
|
||||||
// NewRecorderHandler records all the traffic data
|
// NewRecorderHandler records all the traffic data
|
||||||
func NewRecorderHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc {
|
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
|
// Save req body for later.
|
||||||
reqBody, _ := ioutil.ReadAll(req.Body)
|
reqBody, _ := ioutil.ReadAll(r.Body)
|
||||||
req.Body.Close()
|
r.Body.Close()
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
r.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Record Roundtrip.
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
next.ServeHTTP(rec, req)
|
next.ServeHTTP(rec, r)
|
||||||
|
|
||||||
elapsed := time.Since(start).Truncate(time.Millisecond) / time.Millisecond
|
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() {
|
for k, v := range rec.Header() {
|
||||||
rw.Header()[k] = v
|
rw.Header()[k] = v
|
||||||
}
|
}
|
||||||
rw.WriteHeader(rec.Code)
|
rw.WriteHeader(rec.Code)
|
||||||
rw.Write(rec.Body.Bytes())
|
rw.Write(resBody)
|
||||||
|
|
||||||
// record req and res
|
// Save req and res data.
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
|
||||||
res := rec.Result()
|
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})
|
srv.Insert(Capture{Req: req, Res: res, Elapsed: elapsed})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,68 +230,55 @@ func NewProxyHandler(URL string) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dump(c *Capture) CaptureDump {
|
func dump(c *Capture) CaptureDump {
|
||||||
reqDump, err := dumpRequest(c.Req)
|
req := c.Req
|
||||||
if err != nil {
|
res := c.Res
|
||||||
fmt.Printf("could not dump request: %v\n", err)
|
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) {
|
func dumpContent(header http.Header, body []byte, format string, args ...interface{}) string {
|
||||||
if req.Header.Get("Content-Encoding") == "gzip" {
|
b := strings.Builder{}
|
||||||
return dumpGzipRequest(req)
|
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)
|
sort.Strings(headers)
|
||||||
}
|
for _, v := range headers {
|
||||||
|
dst.WriteString(v)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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.
|
func dumpCurl(req Req) string {
|
||||||
// This body is set in NewProxyHandler() in proxy.ErrorHandler
|
var b strings.Builder
|
||||||
func dumpResponseBody(res *http.Response) ([]byte, error) {
|
// build cmd
|
||||||
resBody, _ := ioutil.ReadAll(res.Body)
|
fmt.Fprintf(&b, "curl -X %s %s", req.Method, req.Url)
|
||||||
res.Body.Close()
|
// build headers
|
||||||
res.Body = ioutil.NopCloser(bytes.NewReader(resBody))
|
for k, v := range req.Header {
|
||||||
return resBody, nil
|
fmt.Fprintf(&b, " \\\n -H '%s: %s'", k, strings.Join(v, " "))
|
||||||
}
|
}
|
||||||
|
// build body
|
||||||
func dumpGzipResponse(res *http.Response) ([]byte, error) {
|
if len(req.Body) > 0 {
|
||||||
resBody, _ := ioutil.ReadAll(res.Body)
|
fmt.Fprintf(&b, " \\\n -d '%s'", req.Body)
|
||||||
res.Body.Close()
|
}
|
||||||
res.Body = ioutil.NopCloser(bytes.NewReader(resBody))
|
return b.String()
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
114
main_test.go
114
main_test.go
|
@ -84,91 +84,49 @@ func PostRequest() TestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDumpRequest(t *testing.T) {
|
func ExampleDump() {
|
||||||
msg := "hello"
|
c := &Capture{
|
||||||
|
Req: Req{
|
||||||
// given
|
Proto: "HTTP/1.1",
|
||||||
req, err := http.NewRequest(http.MethodPost, "http://localhost:9000/", strings.NewReader(msg))
|
Url: "http://localhost/hello",
|
||||||
if err != nil {
|
Path: "/hello",
|
||||||
t.Errorf("Could not create request: %v", err)
|
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
|
fmt.Println(got.Request)
|
||||||
body, err := dumpRequest(req)
|
fmt.Println(got.Response)
|
||||||
|
fmt.Println(got.Curl)
|
||||||
|
|
||||||
// then
|
// Output:
|
||||||
if err != nil {
|
// GET /hello HTTP/1.1
|
||||||
t.Errorf("Dump Request error: %v", err)
|
//
|
||||||
}
|
// Content-Encoding: none
|
||||||
if !strings.Contains(string(body), msg) {
|
//
|
||||||
t.Errorf("Dump Request is not '%s'", msg)
|
// 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) {
|
func gzipStr(str string) []byte {
|
||||||
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 {
|
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
g := gzip.NewWriter(&buff)
|
g := gzip.NewWriter(&buff)
|
||||||
io.WriteString(g, str)
|
io.WriteString(g, str)
|
||||||
g.Close()
|
g.Close()
|
||||||
return buff.String()
|
return buff.Bytes()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue